Androidのアプリ内課金について記載します。Version3は公開されて間もないせいかサンプルが少なく苦労しましたので、手助けになれば幸いです。
以下、実装手順です。
1. プロジェクトにIn-app Billingライブラリを追加します。
TrivialDrivaサンプルからIInAppBillingService.aidlファイルをプロジェクトにコピーします。Eclipseなら/srcディレクトリにインポートしてください。
ビルド後、/genディレクトリ内にIInAppBillingService.javaが作成されていることを確認します。
※TrivialDrivaサンプルの取得方法
・Android SDK ManagerでGoogle Play Billing Libraryをインストールします。
・[sdk-install-dir]/extras/google/play_billing/in-app-billing-v03/samples/TrivialDrive/からプロジェクトをインポートしてください。
2. AndroidManifest.xmlにBILLINGのパーミッションを記述します。
3. プログラムを記述します。
まず、ServiceConnectionを作成します。ServiceConnectionを作成するには、以下の処理がアプリ内で必要になります。
・IInAppBillingServiceにバインドする。
・Google PlayアプリにIPCでbillingリクエストを送る。
・各billingリクエストごとに返ってくる同期レスポンスメッセージを処理する。
次に、IInAppBillingServiceにバインドします。
Google Play のIn-app Billingサービスへの接続を確立するには、ActivityをIInAppBillingServiceにバインドするためのServiceConnectionを実装する必要があります。
onServiceDisconnectedとonServiceConnectedメソッドをオーバーライドし、接続が確立された後にIInAppBillingServiceインスタンスへの参照を取得します。
ServiceConnection mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
@Override
public void onServiceConnected(ComponentName name,
IBinder service) {
mService = IInAppBillingService.Stub.asInterface(service);
}
};
onServiceConnectedは接続が完了した時に、onServiceDisconnectedは何らかの要因で接続が切断された時に呼ばれます。後述のServiceをバインドしてから接続が完了するまで若干時間がかかるため、バインド直後に課金内容を問い合わせる、といった記述にするとエラーになってしまいます。そのため、例えばアプリ起動時に課金内容の問い合わせなどを行いたい場合は、onServiceConnected内に記述すると接続が完了したタイミングで呼び出されるため、エラーが出ることがありません。
続いて、ActivityのonCreateメソッド内で、bindServiceメソッドを呼んでバインドします。メソッドにはIn-app Billingサービスを参照するIntentおよび ServiceConnectionのインスタンスを渡します。
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindService(new
Intent("com.android.vending.billing.InAppBillingService.BIND"),
mServiceConn, Context.BIND_AUTO_CREATE);
Activityが終了する場合、In-app Billingサービスからのアンバインドを行います。これを忘れるとアプリ終了後もサービスのアンバインドが行われないため、Androidのパフォーマンスが低下してしまいます。
public void onDestroy() {
super.onDestroy();
if (mServiceConn != null) {
unbindService(mServiceConn);
}
}
次に、In-app Billingリクエストを作成します。
アプリがGoogle Playに接続されたら、アプリ内課金アイテムのリクエストを開始することができます。
まずは、購入可能なアイテムを問い合わせてみます。In-app Billingサービスにリクエストを送るには、プロダクトIDのString ArrayListを作成し、それを “ITEM_ID_LIST”というキーでBundleに保持します。
skuList.add("premiumUpgrade");
skuList.add("gas");
Bundle querySkus = new Bundle();
querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
※ArrayListにaddしているプロダクトIDは、Google playに登録するアプリ内アイテムのIDです。
Google Playから情報を取得するには、In-app Billing Version 3 APIのgetSkuDetailsメソッドを呼び出します。第1引数には In-app Billing API versionの”3″を、第2引数には呼び出しアプリのパッケージ名を、第3引数には購入タイプの”inapp”を、第4引数には作成したBundleを渡します。
getPackageName(), "inapp", querySkus);
※getSkuDetailsメソッドはネットワークリクエストのトリガーになり、メインスレッドをブロックしてしまうので、メインスレッドで呼ばずに別スレッドから呼んでください。
問い合わせた結果は”DETAIL_LIST”というキーでString ArrayListに格納され、各購入情報はJSON形式の文字列で格納されます。
if (response == 0) {
ArrayList<String> responseList
= skuDetails.getStringArrayList("DETAILS_LIST");
for (String thisResponse : responseList) {
JSONObject object = new JSONObject(thisResponse);
String sku = object.getString("productId");
String price = object.getString("price");
if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price;
else if (sku.equals("gas")) mGasPrice = price;
}
}
アプリ内アイテムの購入処理
アプリから購入リクエストを開始するには、In-app Billing ServiceのgetBuyIntentメソッドを呼び出します。
第1引数にはIn-app Billing API version の “3”を、第2引数には呼び出しアプリのパッケージ名を、第3引数にはproduct IDを、第4引数には購入タイプの “inapp”を、第4引数にはdeveloperPayload文字列を渡します。
developerPayload文字列は、購入情報としてGoogle Playから返してほしい付加的な引数を指定するのに使います。
sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");
リクエストが成功した場合、返ってきたBundleにはRESPONSE_CODEというキーでBILLING_RESPONSE_RESULT_OK (0)が含まれています。また、”BUY_INTENT”というキーで取得できるPendingIntentで購入フローを開始することが出来ます。
購入処理を完了するには、取得したpendingIntentでstartIntentSenderForResultメソッドを呼び出します。
購入データはJSON形式の文字列としてIntentに格納されており、”INAPP_PURCHASE_DATA”というキーで取得できます。
購入処理完了後、結果がonActivityResult()に返ってきます。
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1001) {
int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
if (resultCode == RESULT_OK) {
try {
JSONObject jo = new JSONObject(purchaseData);
String sku = jo.getString("productId");
Toast.makeText(this, "購入成功", Toast.LENGTH_LONG).show();
}
catch (JSONException e) {
Toast.makeText(this, "購入失敗", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
}
}
購入成功時にプリファレンス等に購入情報を保存しておくと、毎回Google playに購入情報を問い合わせる手間が省けます。
購入したアイテムの情報の問合せ
ユーザーによる購入情報を取得するには、In-app Billing Version 3サービスのgetPurchasesメソッドを呼び出します。
第1引数にはIn-app Billing API versionの”3″を、第2引数には呼び出しアプリのパッケージ名を、第3引数には購入タイプの”inapp”を渡します。
Google Playサービスは、デバイスに現在ログインしているユーザーアカウントの購入についてのみ返します。
リクエストが成功した場合、返ってきたBundleにはRESPONSE_CODEというキーでBILLING_RESPONSE_RESULT_OK(0)が含まれます。
また、”INAPP_PURCHASE_ITEM_LIST”というキーでproductIDのリストが、”INAPP_PURCHASE_DATA_LIST”でオーダー詳細のリストが、”INAPP_DATA_SIGNATURE”で各購入のシグネチャのリストが含まれます。
if (response == 0) {
ArrayList ownedSkus =
ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
ArrayList purchaseDataList =
ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
ArrayList signatureList =
ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
購入アイテムの消費
一度アイテムを購入すると、消費処理を行わない限り所有権があると考えられてGoogle Playから同じアイテムを購入することが出来なくなります。そのため、アイテムを複数回購入可能にしたい場合はアイテムの消費リクエストを送らなければなりません。
逆に購入を一度しか行いたくないアイテムの場合は、この処理は必要ありません。
購入アイテムの消費を記録するには、In-app Billing serviceのconsumePurchaseメソッドを呼び出します。
第1引数にはIn-app Billing API versionの “3”を、第2引数には呼び出しアプリのパッケージ名を、第3引数にはpurchaseToken文字列を渡します。purchaseTokenは成功した購入リクエストでGoogle Play サービスから返される”INAPP_PURCHASE_DATA”文字列の一部として含まれます。
※consumePurchaseもgetSkuDetails同様メインスレッドをブロックしてしまうので、別スレッドから呼び出してください。
課金テストのために作成したテストアプリのソースは以下の通りです。
Handler handler = new Handler();
private IInAppBillingService mService;
private Button buttonKakin;
private Button buttonToiawase;
private Context context;
private TextView textKakin;
ServiceConnection mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
@Override
public void onServiceConnected(ComponentName name,
IBinder service) {
mService = IInAppBillingService.Stub.asInterface(service);
Toast.makeText(context, "接続完了", Toast.LENGTH_LONG).show();
try {
Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);
int response = ownedItems.getInt("RESPONSE_CODE");
Toast.makeText(context, "response:"+response, Toast.LENGTH_LONG).show();
if (response == 0) {
ArrayList<?> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
SharedPreferences p = PreferenceManager
.getDefaultSharedPreferences(context);
Editor ed = p.edit();
int flag = ed.getInt("unLockFlag2",0);
if(flag == 0){
if(ownedSkus.size() != 0){
if(ownedSkus.get(0).equals("test")){
textKakin.setText("Google Playに問い合わせた結果、購入済みです");
textKakin.invalidate();
ed.setInt("unLockFlag2", 1);
ed.commit();
}else{
textKakin.setText("Google Playに問い合わせた結果、未購入です");
textKakin.invalidate();
ed.setInt("unLockFlag2", 2);
ed.commit();
}
}else{
textKakin.setText("Google Playに問い合わせた結果、未購入です");
textKakin.invalidate();
ed.setInt("unLockFlag2", 2);
ed.commit();
}
}else if(flag == 1){
textKakin.setText("Preferencesに問い合わせた結果、購入済みです");
textKakin.invalidate();
}else if(flag == 2){
textKakin.setText("Preferencesに問い合わせた結果、未購入です");
textKakin.invalidate();
}
}
} catch (RemoteException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = this;
buttonKakin = (Button) findViewById(R.id.ButtonKakin);
buttonToiawase = (Button) findViewById(R.id.ButtonToiawase);
textKakin = (TextView) findViewById(R.id.TextKakin);
bindService(new Intent("com.android.vending.billing.InAppBillingService.BIND"),mServiceConn, Context.BIND_AUTO_CREATE);
ArrayList<String> skuList = new ArrayList<String>();
skuList.add("test");
final Bundle querySkus = new Bundle();
querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
buttonKakin.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
// TODO 自動生成されたメソッド・スタブ
try {
Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(),
"test", "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
startIntentSenderForResult(pendingIntent.getIntentSender(),
1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
} catch (RemoteException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
} catch (SendIntentException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
handler.post(new Runnable() {
public void run() {
try {
Bundle skuDetails = mService.getSkuDetails(3,getPackageName(), "inapp", querySkus);
int response = skuDetails.getInt("RESPONSE_CODE");
Toast.makeText(context, "response:"+response, Toast.LENGTH_LONG).show();
if (response == 0) {
ArrayList<String> responseList
= skuDetails.getStringArrayList("DETAILS_LIST");
for (String thisResponse : responseList) {
JSONObject object = new JSONObject(thisResponse);
String sku = object.getString("productId");
String price = object.getString("price");
}
}
} catch (RemoteException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
} catch (JSONException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
}
});
}
}).start();
}
});
buttonToiawase.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
try {
Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);
int response = ownedItems.getInt("RESPONSE_CODE");
Toast.makeText(context, "response:"+response, Toast.LENGTH_LONG).show();
if (response == 0) {
ArrayList<?> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
ArrayList<?> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
ArrayList<?> signatureList = ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
String continuationToken = ownedItems.getString("INAPP_CONTINUATION_TOKEN");
System.out.println("ownedSkus:"+ownedSkus);
System.out.println("purchaseDataList:"+purchaseDataList);
System.out.println("signatureList:"+signatureList);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = (String) purchaseDataList.get(i);
String sku = (String) ownedSkus.get(i);
}
}
} catch (RemoteException e) {
// TODO 自動生成された catch ブロック
e.printStackTrace();
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1001) {
int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
if (resultCode == RESULT_OK) {
try {
JSONObject jo = new JSONObject(purchaseData);
String sku = jo.getString("productId");
Toast.makeText(this, "購入成功:"+sku, Toast.LENGTH_LONG).show();
if(sku.equals("test")){
SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context);
Editor ed = p.edit();
ed.setInt("unLockFlag2", 1);
ed.commit();
textKakin.setText("購入しました。");
textKakin.invalidate();
}
}
catch (JSONException e) {
Toast.makeText(this, "購入失敗", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mServiceConn != null) {
unbindService(mServiceConn);
}
}
}
購入開始ボタンと購入状況を問い合わせるボタン、現在の購入状況を表示するTextViewを設置しています。初回起動時にはGoogle Playに購入状況を問い合わせ、指定のプロダクトIDの購入状況をプリファレンスに保存し、次回起動時はプリファレンスから購入状況を取得しています。そのため一度アプリをアンインストール後、再インストールしても初回起動時にGoogle Playに問い合わせるため購入状況は保持されます。
参考資料
Implementing In-app Billing (IAB Version 3)
Android アプリ内課金(API Version3)の実装