【Android】GooglePlayによるアプリ内課金実装方法

JUnitについての書籍が遂に発売します!
Java開発時のユニットテストを加速する「JUnit速効レシピ」


Androidアプリにて課金処理を行う方法を解説します。
アプリの重要な収入源となりますので、なるべく習得しておきたい技術ですね。

Androidアプリにて課金を行うには、GooglePlayの機能を使います。

まずはGooglePlay決済に必要なモジュールをインストールしましょう。
専用のモジュールとして『IInAppBillingService』が用意されています。

  1. IInAppBillingServiceモジュールのインストール
  2. IInAppBillingServiceモジュールに接続しよう
  3. GooglePlayの課金の仕組みとは?
  4. 商品情報の詳細を取得しよう
  5. 開発段階で、テスト用のプロダクトIDを用意出来ない場合は?
  6. 商品を購入しよう
  7. ユーザの注文情報を取得しよう
  8. 過去に購入した商品の消費を通知しよう


IInAppBillingServiceモジュールのインストール




モジュール自体は何処かのWebサイトからダウンロードするのではなく、
SDKマネージャーからダウンロードさせます。

SDKマネージャーを起動し、『Extras』の中にあります『Google Play Billing Library.』にチェックを入れ
『install』ボタンを押してインストールを行います。







インストールが完了致しましたら、
インストール先の
(sdk)/extras/google/play_billing/IInAppBillingService.aidl

(アプリ)/src/com/android/vending/billing/IInAppBillingService.aidl
へ配置し、ビルドして下さい。

そうすると、
(アプリ)/gen/com/android/vending/billing/IInAppBillingService.java

が自動的に作成されるかと思います。
(自動的に生成されない場合は、設置場所があっているか?正しいファイルかを確認して下さい。


無事に上記ファイルが自動生成されればインストール完了です。
後は実際に課金に関わる処理を実装して行きます。





IInAppBillingServiceモジュールに接続しよう




それでは早速、先程インストールして配置したモジュールへ接続し、
GooglePlayへ課金リクエストを送信する準備を行いましょう。


その前に、いつもの如くパーミッション設定を『AndroidManifest.xml』へ追記する必要があります。
アプリ内課金にGooglePlayを使用する際のパーミッション設定は、





上記を追記しましたら、ようやく本実装コードに触れて行きます。




実際に、IInAppBillingServiceへ接続し、
インスタンスを保持するサンプルコードを下記に示します。

まずは接続結果を保持する為に、メンバ変数に下記二点を追加します。


// 決済モジュール
private IInAppBillingService billingService = null;
private ServiceConnection serviceConnection = null;


そして、決済モジュールへのコネクションを作成してバインド、
接続が確立されたら、課金サービスのインスタンスを保持しておきます。

そして、接続が切断されたら保持しておいたインスタンスを破棄する所まで
実装します。



// 決済モジュールへのコネクションを作成
this.serviceConnection = new ServiceConnection(){
 @Override
 public void onServiceConnected( ComponentName paramComponentName, IBinder paramIBinder ){

  // 接続が確立されたらインスタンを保持しておく
  this.billingService = IInAppBillingService.Stub.asInterface( paramIBinder );
 }
 @Override
 public void onServiceDisconnected( ComponentName paramComponentName ){

  // 接続が切断されたらインスタンスを吐きしておく
  this.billingService = null;
 }
};
 
// バインドする
this.bindService(
 new Intent("com.android.vending.billing.InAppBillingService.BIND"),
 this.serviceConnection,
 Context.BIND_AUTO_CREATE
);


まずは決済モジュールとのコネクションを作成し、
作成したコネクションをを使って、アクティビティと決済モジュールを結びつける為に、
『bindService()』にてバインドします。
バインドが成功し、決済モジュールへの接続が完了すると、
『onServiceConnected()』が起動され、
引数として渡されて来ます『IBinder』を元に取得出来る
『IInAppBillingService』のインスタンスを保持しておきます。

接続が切断されたら、不要になるインスタンスへnullを入れる事で破棄しています。


バインドは、アクティビティと結びついていますので、
アクティビティが破棄されるタイミングで、バインドも解除する必要があります。

バインドの解除は、アクティビティが終了するタイミングで呼ばれる
『onDestroy()』内にて行うのが良いでしょう。


@Override
public void onDestroy()
{
 if( this.serviceConnection != null ){
  unbindService( this.serviceConnection );
 }
 super.onDestroy();
}



後は、接続が成功した時に保持した『this.billingService』を使用して、
様々な情報のやり取りをGooglePlayと行います。




GooglePlayの課金の仕組みとは?




先程作成したGooglePlayへのコネクションを通してやり取りする事の出来る事は、
大きく分けて四つあります。

一つ目が、課金対象の商品情報を取得する。
二つ目が、課金対象の商品を購入する。
三つ目が、ユーザの購入履歴を取得する。
四つ目が、購入済みの商品が消費された事を通知する。

三つ目までは、一般的な処理なので説明は不要かと思いますが、
商品名や概要、金額等を取得するAPIと、実際に商品購入のリクエストを送信するAPI。
そして、今までユーザが購入した商品を確認する事の出来るAPI。

そして、四つ目のAPIが少しわかりにくいかと思いますが『消費』という概念を持ったAPIです。

GooglePlayでは、一度購入すると、その商品はユーザが『所有している』とみなし、
もう一度同じ商品を購入する事は出来ません。
そこで、『商品は既に消費され、新たに購入出来ますよ』とGooglePlayに知らせる必要があります。
例えば、アプリ内コインを購入したとして、購入完了後にユーザ情報へ
購入したコインを適用したら、それは消費とみなし、消費用APIにて通知する事で
同じ商品を再度購入できる用に出来ます。

この、消費のタイミングはアプリが自由に指定出来ますので、
例えば『期間』を商品にする場合は、一ヶ月たったら消費にするとか、
二ヶ月たったら消費にするとか、とても自由に構築する事が可能です。

また、これらの処理はメインスレッドとは別にスレッドを用意して行うべきです。
そうでないと、ネットワークリクエストによってメインスレッドがロックされてしまいます。


これらの点を踏まえた上で、四つそれぞれのAPIの使用方法と、返却値等についての解説を
次章より続けたいと思います。




商品情報の詳細を取得しよう




まずは、課金商品に対する情報を取得してみましょう。
基本的に、課金商品はGooglePlay上にプロダクトID毎に管理されています。

商品情報は、Jsonとして取得する事が出来、
キーと取得出来る内容は下記の通りです。


productIdプロダクトID
type商品タイプ
price金額
titleタイトル
description概要




実際にGooglePlayへ、取得したい商品のプロダクトIDを元に問い合わせを行い、
商品情報を取得するサンプルコードを下記に示します。




// 問い合わせる商品IDを纏める
ArrayList request_id_list = new ArrayList();
request_id_list.add( "プロダクトID" );
Bundle query = new Bundle();
query.putStringArrayList("ITEM_ID_LIST", request_id_list);

// リクエストに対するレスポンスを取得する
Bundle details = this.billingService.getSkuDetails(3, AppBillingActivity.this.getPackageName(), "inapp", query);

// レスポンスコードを取得する
responseCode = details.getInt("RESPONSE_CODE");

// 成功
if( responseCode == BILLING_RESPONSE_RESULT_OK ){

 // 商品リストを取得する
 ArrayList response_list = details.getStringArrayList("DETAILS_LIST");

 for( String row : response_list ){

  // JSONオブジェクトへ変換する
  JSONObject object = new JSONObject( row );

  // 商品情報をログに出力する
  Log.v("result", "productId = "+object.getString("productId") );
  Log.v("result", "type = "+object.getString("type") );
  Log.v("result", "price = "+object.getString("price") );
  Log.v("result", "title = "+object.getString("title") );
  Log.v("result", "description = "+object.getString("description") );
 }
}


問い合わせる商品のプロダクトIDの配列を作成し、『ITEM_ID_LIST』というキーでバンドルへ登録。
そのバンドルを使用して商品情報取得のリクエストを行います。
具体的な問い合わせメソッドは『getSkuDetails()』を使い、
引数として指定するのは、

・使用する決済モジュールのAPIバージョン
・呼び出し元のパッケージ名
・購入タイプとして文字列を指定。通常購入⇒『inapp』、定期購入⇒『subs』
・そしてプロダクトIDを纏めたバンドル

を指定します。


リクエストに対する返却はバンドルにて渡され、『RESPONSE_CODE』というキーで
結果ステータスを取得する事が出来ます。

判別する事の出来るステータスを一元で管理出来るように、
私はメンバとしてステータスコードと定数を纏めて定義しています。

コードと内容は、下記の宣言を参考にして下さい。




// 成功
public static final Integer BILLING_RESPONSE_RESULT_OK     = 0;

// ダイアログをキャンセルしたか、戻るボタンが押された事によるキャンセル
public static final Integer BILLING_RESPONSE_RESULT_USER_CANCELED  = 1;

// 課金APIのバージョンがサポート外
public static final Integer BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;

// リクエストした商品は購入出来ない
public static final Integer BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;

// 引数が無効、もしくは署名が不正、課金設定がGooglePlayで不備、AndroidManifestに権限の記述無し。
public static final Integer BILLING_RESPONSE_RESULT_DEVELOPER_ERROR  = 5;

// 処理中に致命的なエラーが発生
public static final Integer BILLING_RESPONSE_RESULT_ERROR    = 6;

// 既に所有している商品に対しての購入リクエストで失敗
public static final Integer BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;

// 所有していない商品に対して消費行動を行おうとして失敗
public static final Integer BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED  = 8;



ステータスコードの他に、
詳細情報は『getStringArrayList()』にキーとして『DETAILS_LIST』を
渡す事で配列として取得する事が出来ます。

取得した配列を『for』で回しながらJSONObjectを作成し、
『getString()』にて該当するキーを指定して値を取得します。




商品情報の一連の流れを纏めますと、

・プロダクトIDの配列を作り、バンドルを作成
・商品情報取得のリクエストを送信
・レスポンスコードで成功を判別
・詳細情報を取得
・詳細情報は配列で取得出来るので、個別の値を取得するにはforで回す
・情報はJSONObjectへ整え、『getString()』にてキー指定で値を得る。



これで詳細情報を知りたい商品のプロダクトIDを元に、
商品詳細情報を取得出来ます。





開発段階で、テスト用のプロダクトIDを用意出来ない場合は?




実際の商品をGooglePlayへ登録して、テストするのは少し面倒です。
そこで、テストに使えるプロダクトIDが標準で用意されています。

用意されているプロダクトIDは四つあり、それぞれの用途によって使い分ける事が出来ます。

android.test.purchased正常に購入出来るテスト用プロダクトID
android.test.canceled購入のキャンセルをテストできるプロダクトID
android.test.refunded払い戻しが行われた時のレスポンスをテストできるプロダクトID
android.test.item_unavailable購入希望商品が存在しなかった時をシミュレートできるプロダクトID


準備出来ない方は、上記のプロダクトIDを使い分けて
効果的にテストするのが効率的かも知れません。






商品を購入しよう



いよいよ、アプリ内課金の本番、商品の購入処理を実装しましょう。
流れと致しましては、購入リクエストを送信し、
購入処理を行う。購入用のアクティビティが終了する時に、
アクティビティの『onActivityResult()』が起動されるので、
そこから購入情報を取得する。

慣れればシンプルでとても解りやすいと思います。

まずは実際のコードを見てみましょう。

// 購入リクエストを送信する(APIVersion,パッケージ名,productID,購入タイプ,任意の文字列)
Bundle buy_intent_bundle = this.billingService.getBuyIntent(3, this.getPackageName(),
  "プロダクトID", "inapp", "developerPayload");

// レスポンスコードを取得する
responseCode = buy_intent_bundle.getInt( "RESPONSE_CODE" );

// 成功(購入可能)
if( responseCode == BILLING_RESPONSE_RESULT_OK ){ 

 // 購入フローを開始する
 PendingIntent pending_intent = buy_intent_bundle.getParcelable("BUY_INTENT"); 

 // 購入フローを完了する
 this.startIntentSenderForResult(pending_intent.getIntentSender(),
   1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
   Integer.valueOf(0));  

 // アクティビティのonActivityResultに結果が返却される
}



上記コードが、購入のリクエストから購入フローの開始から終了までの実装です。
購入リクエストを行うのには『getBuyIntent()』を使い、
引数として指定するのは、

・使用する決済モジュールのAPIバージョン
・呼び出し元のパッケージ名
・プロダクトID
・購入タイプとして文字列を指定。通常購入⇒『inapp』、定期購入⇒『subs』
・任意の補足情報文字列

を指定します。



画面イメージとしては、決済ダイアログが自動で出現し、課金処理を行ってくれます。
課金処理が完了したら、課金用のアクティビティは破棄されますので、
購入情報を持ったコールバックとして『onActivityResult()』が起動されます。

『onActivityResult()』内では、実際に購入情報の取得を行うサンプルコードを下記に記しましたので、
参考にして下さい。



@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
 // 返却コードを判別
 if( requestCode == 1001 ){

  // 結果ステータスを取得
  int responseCode = data.getIntExtra( "RESPONSE_CODE", 0 );

  // 成功
  if( resultCode == BILLING_RESPONSE_RESULT_OK ){

   // 注文情報を取得する
   String purchase_data = data.getStringExtra("INAPP_PURCHASE_DATA");

   // JSONオブジェクトへ変換する
   JSONObject object = new JSONObject( purchase_data );

   // 注文情報をログに出力する
   Log.v("result", "orderId = "+object.getString("orderId") );
   Log.v("result", "packageName = "+object.getString("packageName") );
   Log.v("result", "productId = "+object.getString("productId") );
   Log.v("result", "purchaseTime = "+object.getString("purchaseTime") );
   Log.v("result", "purchaseState = "+object.getString("purchaseState") );
   Log.v("result", "developerPayload = "+object.getString("developerPayload") );
   Log.v("result", "purchaseToken = "+object.getString("purchaseToken") );
  }
 }

}



購入結果のステータスとは別に、『INAPP_PURCHASE_DATA』をキーとして注文情報もJsonにて返却されてきます。
取得出来る注文情報の内容とキーは下記を参照下さい。


orderId注文番号
packageNameアプリのパッケージ名
productId商品番号
purchaseTime購入日時
purchaseState購入状態(0:購入 1:キャンセル 2:返金)
developerPayload購入時に指定した任意の補足情報文字列
purchaseToken商品とユーザを結びつけるトークン







ユーザの注文情報を取得しよう




現在端末でログインしているユーザの購入履歴を取得する事が出来ます。
取得出来る情報は、購入の時に取得出来る注文情報と同じです。
それが、過去に渡っての注文履歴を纏めて取得できます。

それでは早速実装サンプルです。


// 現在端末でログインしているアカウントを元に問い合わせる
Bundle owned_items = this.billingService.getPurchases( 3, this.getPackageName(), "inapp", null );

// レスポンスコードを取得する
responseCode = owned_items.getInt( "RESPONSE_CODE" );

// 成功
if( responseCode == BILLING_RESPONSE_RESULT_OK ){

 // 注文情報のリストを取得する
 ArrayList data_list = owned_items.getStringArrayList( "INAPP_PURCHASE_DATA_LIST" );

 // 注文情報をまとめ上げる
 for( int i = 0; i < data_list.size(); ++i ){

  // 情報を取得
  String row = (String) data_list.get(i);

  // JSONオブジェクトへ変換する
  JSONObject object = new JSONObject( row );

  // 注文情報をログに出力する
  Log.v("result", "orderId = "+object.getString("orderId") );
  Log.v("result", "packageName = "+object.getString("packageName") );
  Log.v("result", "productId = "+object.getString("productId") );
  Log.v("result", "purchaseTime = "+object.getString("purchaseTime") );
  Log.v("result", "purchaseState = "+object.getString("purchaseState") );
  Log.v("result", "developerPayload = "+object.getString("developerPayload") );
  Log.v("result", "purchaseToken = "+object.getString("purchaseToken") );
 }
}



購入履歴取得リクエストを行うのには『getPurchases()』を使い、
引数として指定するのは、

・使用する決済モジュールのAPIバージョン
・呼び出し元のパッケージ名
・購入タイプとして文字列を指定。通常購入⇒『inapp』、定期購入⇒『subs』
・null

を指定します。


取得に成功すると、『INAPP_PURCHASE_DATA_LIST』をキーに
購入履歴を取得できますので、『for』で順に取り出す事で、
JSON解析して内容を確認する事が出来ます。







過去に購入した商品の消費を通知しよう




一度購入した商品は、消費しない限り、再度購入する事は出来ません。
これは、一度購入したら所有権が既にあり、重複購入を防ぐ意図があるかと思います。

この章では、そんな商品の消費をGooglePlayへ通知するやり方を解説いたします。


// 消費リクエストを送信する
responseCode = this.billingService.consumePurchase(3, this.getPackageName(), "トークン");

// 成功
if( responseCode == BILLING_RESPONSE_RESULT_OK ){
}


商品の消費を通知する為のリクエストはとてもシンプルです。
リクエストを送信すると、結果コードが返却されてきますので、
実行結果に応じて処理を追加してください。

リクエストの方法は、『consumePurchase()』を使用し、
引数として指定するのは、

・使用する決済モジュールのAPIバージョン
・呼び出し元のパッケージ名
・ユーザと商品を結びつけるトークン


を指定します。


『ユーザと商品を結びつけるトークン』というのは、購入した時や、購入履歴を取得した際に
『purchaseToken』というキーにて得られる値です。
どのユーザの持っているどの商品が消費されたのか?というのをGooglePlayでは判断する必要がありますので、
『ユーザと商品を結びつけるトークン』を渡す必要があるんですね。

このトークンを取得、もしくは保持しておく必要があるので、
消費リクエスト自体は簡単でも、前段階としての準備が少し手間ですね。






これでAndroid端末でもアプリ内課金を実装する事が出来ます。
GooglePlayとの連携は、今後のアプリ開発において収益面でとても重要な役割を担いますので、
基本的な仕組みをしっかりと身につけて頂ければ幸いです。






JUnitについての書籍が遂に発売します!
Java開発時のユニットテストを加速する「JUnit速効レシピ」

9 Responses to 【Android】GooglePlayによるアプリ内課金実装方法

  1. はじめまして。個人でandroidアプリの開発をしているものです。

    ためになる記事がおおく、濱田さんの記事はいつも参考にさせてもらっています。
    そして、今回はじめてアプリ内課金を実装したアプリを開発していて、こちらの記事を
    読ませていただきながら実装に取り組んでいるのですが、どうしても途中でつまってしまいます。

    IInAppBillingServiceとの接続を確立する、のコードや
    商品の詳細情報を取得する、のコードは
    どこに書けばいいのでしょうか?
    Activityを継承したクラスのOnCreateの中なのか、
    別でサービスクラスなどをつくるのか教えていただきたいです。

    質問の意図からして間違っているかもしれませんが、どうかご教授願います。

    返信削除
    返信
    1. ぽにあん 様

      この度はコメント頂き、誠に有難うございます。
      また、いつも当ブログへお越しくださいまして有難うございます。
      アプリ内課金コードの記述場所ですが、
      私の場合は汎用的にアプリ内課金に関するコードを全て持っている、
      課金クラス『AppBillingActivity』というのを、
      『Activity』を継承して作成しています。
      使い方としては、課金を利用するであろうアクティビティを作成する際に、
      『Activity』を継承するのではなく、
      予め作成しておいた『AppBillingActivity』を継承します。
      後はアプリ内課金を行う時に、親の課金メソッドを起動します。

      こうしておくと、いろんな画面やアプリで使いまわせるので、
      便利かと思います。

      参考にして頂ければ幸いです。
      今後ともよろしくお願いいたします。

      削除
    2. 迅速な対応ありがとうございます。

      Activityを継承したクラスをつくっておき、
      それを課金を行いたいクラスに継承するのですね。
      もう一度、参考にさせていただきながら、進めてみます。

      ありがとうございました。

      削除
    3. ぽにあん 様

      この度はお返事頂き、誠に有難うございます。

      >Activityを継承したクラスをつくっておき、
      >それを課金を行いたいクラスに継承するのですね。

      はい。私はその様にして使用しています。
      ですので、特にOnCreate等を意識する必要はなく、
      汎用的に使いまわせると思います。

      他にも、何かご不明な点やご要望、ご相談等御座いましたら
      お気軽にコメント下さい。
      今後ともよろしくお願いいたします。

      削除
    4. お世話になっております。
      何度も質問すみません。。。
      なんとか課金クラスをつくり、テスト用プロダクトIDでのテストを経て、
      実際にgooglePlayに登録してあるプロダクトIDでのテスト購入まですることができました。

      しかし、一つ疑問がありまして濱田さんの場合
      onActivityResultメソッドの中で
      resultcode==BILLING_RESPONSE_RESULT_OK(定数で0)
      としておられますが、購入が成功しても私の場合-1が返ってきます。

      これはテスト用アカウントだからなのか、成功していないのか
      分からないのですが、ご教授いただけないでしょうか?

      削除
    5. ぽにあん 様

      この度はご連絡頂きまして、誠に有難うございます。
      getIntExtra("RESPONSE_CODE", 0)で取得出来る値に、-1が用意されているか調べた所、
      その様なレスポンスコードは確認出来ませんでした。

      GooglePlayが用意しているレスポンスコード以外が取得出来てしまう事は
      異常ですので、取得先が違うのかも知れません。
      もちろん取得に失敗したら設定したデフォルト値が入りますので、
      何処かから「正しく」-1が返却されて来ているのかと思います。

      もう一度、onActivityResultを読んでいるのがGooglePlayなのか確認して見てください。

      具体的な解決案を提示できず申し訳ございません。

      他にも、何かご不明な点が御座いましたらお気軽にコメント下さい。
      今後ともよろしくお願いいたします。

      削除
    6. お返事ありがとうございます。

      取得先の確認など、確かめながらもういちど一つ一つやってみます。

      お忙しい中、時間を割いて説明していただき本当にありがとうございました。
      まだ、Androidで手がいっぱいですが、Webアプリケーションにも興味がありますので
      その勉強のときには、濱田さんの書籍を利用させていただきたいと思ってます。

      削除
    7. ぽにあん 様

      この度はご連絡頂きまして、誠に有難うございます。
      また、何か御座いましたらお気軽にコメント下さい。
      今後とも宜しくお願い致します。

      削除
  2. こんばんは

    定期購入をアプリに導入しようと考えていますが、初心者のため実装ができていません。
    クラスを公開していただくことは可能でしょうか?

    また、月額課金の場合、テストするには一か月ただ待っている必要がありますでしょうか?

    それともテストの場合だけすぐに確認できる方法はありますでしょうか?

    よろしくお願いいたします。

    返信削除

人気の投稿

Category

Algorithm (2) Android (8) ASP/aspx (1) Blogger (2) C/C++ (1) Chrome (5) CSS (9) Firefox (4) Fortran (1) Google (9) GoogleMap (2) HTML (12) IE (3) Information (4) iOS (2) iPhone/iPad/iPod (2) Java (6) JavaScript (16) jQuery (9) JSP (1) LifeRecipe (5) Linux (2) Macintosh (2) MapKit (4) Marketing (7) MySQL (3) NAMAZU (2) Objective-C (7) Other (7) Perl (1) PHP (9) Python (1) RSS/Atom (2) Ruby (1) Safari (2) SEO (11) Smarty (2) SQL (2) Tex (1) Three.js (1) Twitter (1) TwitterLog (313) UIKit (5) Unix (1) VBA/VBS (1) Windows (5) WordPress (3) Writing (5) XAMPP (1) XML (1) Yahoo (2) ZendFramework2 (14)

Archives