appmartアプリ内課金V2はgoogle play IAB V3に合わせて作られたものとなります。google play IAB同様に全てのサービスが管理されており、同じサービスを2回購入する場合は購入を「消費」しなければなりません。
デベロッパー自身がServiceに接続し**AIDLの各メソッドを実装する(AIDL方式)かHelperライブラリーを使う(Helper方式)**という二つの実装方法があります。androidのアプリ内課金に慣れていない方またはgoogle V3の経験を持っている方はHelper方式をおすすめします。完全にカスタマイズしたい方はAIDL方式をおすすめします。
デベロッパー様のローカル環境に本プロジェクトをダウンロードしていただき、libraryとして参照します。
先ずはgithubプロジェクトをローカルでcloneしてください。:
cd /home/user/your_directory
git clone https://github.com/info-appmart/appmart-inbilling-as-project
androidプロジェクトとしてworkspaceに導入します:
- ⇒ File
- ⇒ Import
- ⇒ Existing Android Code Into Workspace
- ⇒ 先ほどcloneしたプロジェクトを選択
注意点: Eclipseにうまく読み込まれないために、workspace以外のフォルダーにcloneしてください。
libraryとしてプロジェクトを導入します:
- ⇒ プロジェクトに右クリック
- ⇒ Properties
- ⇒ Android
- ⇒ Libraries : Add ([appmart-inbilling-as-project]を選択)
appmartアプリ内課金V2を利用するには下記permissionsを追加してください。
<!-- 課金API用 -->
<uses-permission android:name="jp.app_mart.permissions.APPMART_BILLING" />
ライブラリー(Helper)を導入することによってappmartアプリ内課金V2との接続を実装する必要がございません。
google play IAB V3同様にhelperが用意されており、簡単にサービス情報・購入情報の取得と購入処理と消費処理を実装することができます。
各メソッドの引数情報は「参照」をご確認ください。
Helperクラスをインスタンス化します:
AppmartIabHelper mHelper= new AppmartIabHelper(this, "your_application_id", "your_license_key");
startSetupメソッドでhelperの初期設定を行います。
mHelper.startSetup(new AppmartIabHelper.OnIabSetupFinishedListener() {
//設定完了後呼ばれるcallback
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()){
if(result.getResponse() == AppmartIabHelper.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE)
debugMess("appmartを更新してください。");
Log.d("appmart", "appmartアプリ内課金:問題が発生しました。" + result);
}else{
Log.d("appmart", "appmartアプリ内課金:設定完了");
}
}
});
上記のコードで設定が完了になります。設定が完了した後にonIabSetupFinished(callback)が呼ばれます。
onDestroyメッソドでappmartのアプリ内課金serviceと切断します:
@Override
public void onDestroy() {
super.onDestroy();
if (mHelper != null) mHelper.dispose();
mHelper = null;
}
inventoryを取得することによってエンドユーザーの購入履歴とサービス情報を取得することができます。 googleのアプリ内課金V3同様にqueryInventoryAsyncで情報を取得できます。(UIThreadでも呼び出し可能です)
消費されたサービスは購入履歴に入りませんのでご注意ください !
//取得したいサービスのID
List<String> additionalSkuList = new ArrayList<String>();
additionalSkuList.add("my_service_id_1");
additionalSkuList.add("my_service_id_2");
// Inventoryを取得
mHelper.queryInventoryAsync(additionalSkuList, mQueryFinishedListener);
// inventory取得後のcallback
AppmartIabHelper.QueryInventoryFinishedListener
mQueryFinishedListener = new AppmartIabHelper.QueryInventoryFinishedListener() {
// Inventory取得後のcallback
public void onQueryInventoryFinished(IabResult result, Inventory inventory){
if (result.isFailure()) {
if(result.getResponse() == AppmartIabHelper.BILLING_RESPONSE_USER_NOT_LOG){
debugMess("appmartでログインしてください。");
}else{
debugMess(result.getMessage());
}
return;
}
// TODO UIを更新
String applePrice = inventory.getSkuDetails(SKU_APPLE).getPrice();
/**
購入の取得:inventory.getPurchase()
サービス情報の取得:inventory.getAllSkuDetails()
*/
}
}
サービスを購入する際に、launchPurchaseFlowを使ってください。
mHelper.launchPurchaseFlow(this,"service_id", 10001,mPurchaseFinishedListener,"test string");
// 決済完了後 callback
AppmartIabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener
= new AppmartIabHelper.OnIabPurchaseFinishedListener() {
// 決済完了後 callback
public void onIabPurchaseFinished(IabResult result, Purchase purchase){
if (result.isFailure()) {
Log.d(TAG, "購入エラー: " + result);
return;
}
debugMess("サービスが購入されました (" + purchase.getSku() + ")");
// TODO UI更新
}
};
決済情報を正常に受け取るために下記のようにonActivityResultを変更してください。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}else {
Log.d(TAG, "onActivityResult handled by AppmartIabHelper.");
}
}
googleアプリ内課金V3同様に全てのサービスが管理されており、同じサービスを購入する前に必ず購入を消費しなければなりません。 過去購入されたサービスを消費するにはConsumeメソッドを使ってください。
継続決済の場合は「消費」は不可能になります!
// inventoryで購入履歴をチェック
if(mInventory.hasPurchase(entry.getSku())){
Purchase p = mInventory.getPurchase(entry.getSku());
mHelper.consumeAsync(p, mConsumeFinishedListener);
}else{
Log.d(mActivity.getClass().getSimpleName(), "未消費の情報がございません。");
}
// サービス消費後 callback
AppmartIabHelper.OnConsumeFinishedListener onConsumeFinishedListener = new AppmartIabHelper.OnConsumeFinishedListener(){
@Override
public void onConsumeFinished(Purchase purchase,IabResult result) {
if(result.isFailure()){
debugMess("サービスが消費されませんでした。");
}else{
debugMess("サービスが消費されました。");
//TODO UIを更新
}
}
};
mHelper.queryInventoryAsync(true,additionalSkuList, mQueryFinishedListener);
Inventory情報を取得
n | 型 | 参考 |
---|---|---|
1 | List | 取得希望ItemのID |
2 | QueryInventoryFinishedListener | 取得後のcallback |
callbackの二つ目のparameterはinventoryです。inventoryで簡単に過去購入されたサービス(未消費)と指定されたサービス情報を取得できます。詳細はInventoryクラスをご確認ください。
mHelper.launchPurchaseFlow(this,"service_id", 10001,mPurchaseFinishedListener,"test string");
決済を実行
n | 型 | 参考 |
---|---|---|
1 | Activity | Activity |
2 | String | サービスID |
3 | String | onActivityResult用のresponse_code |
4 | OnIabPurchaseFinishedListener | 決済完了後のcallback |
5 | String | 決済完了後にreturnされる値(任意) |
mHelper.consumeAsync(p, mConsumeFinishedListener);
過去購入されたサービスを消費
n | 型 | 参考 |
---|---|---|
1 | Purchase | queryInventoryAsyncで取得したpurchase |
2 | OnConsumeFinishedListener | 消費完了後のcallback |
Appmartの課金システムサービスとやりとりするために、AIDLファイルを作成する必要があります。
package名 | class名 |
---|---|
jp.app_mart.service | InAppBillingV2Interface.aidl |
package jp.app_mart.service;
import android.os.Bundle;
interface InAppBillingV2Interface {
/**
* バージョンがサポートされているかチェック
* @param apiVersion アプリ内課金バージョン
* @return : 0=成功 、 その他=エラー
*/
int isBillingSupported(int apiVersion);
/**
* リターンされるPendingIntentをstartし、決済画面へ遷移
* @param apiVersion アプリ内課金バージョン
* @param appId アプリID
* @param sku サービスID
* @param licenceKey ライセンスキー
* @param developerPayload 決済完了リターンされるパラメータ(任意項目)
* @return Bundle 下記情報がリターン:
* "RESPONSE_CODE" 0=成功 、 その他=エラー
* "BUY_INTENT" - 決済画面へ遷移されるためのPendingIntent
*
* 注意点: 必ずstartIntentSenderForResultでPendingIntentをstartしてください。
* onActivityResultの戻り値:
* "RESPONSE_CODE" 0=成功 、 その他=エラー
* "INAPP_PURCHASE_DATA" - 決済情報(JSON形式):
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - 購入キー
*/
Bundle getBuyIntent(int apiVersion, String appId, String sku, String licenceKey, String developerPayload);
/**
* 購入可能なサービス情報を取得(JSON形式)
* @param apiVersion アプリ内課金バージョン
* @param appId アプリID
* @param licenceKey ライセンスキー
* @param skusBundle 検索したいサービスID("ITEM_ID_LIST"):StringArrayList
* @return 下記情報を持つBundleがリターン
* "RESPONSE_CODE" 0=成功 、 その他=エラー
* "DETAILS_LIST" 購入可能なサービスの情報(JSON形式)
* '{ "productId" : "exampleSku", "price" : "500",
* "title : "アプリ名", "description" : "アプリ詳細情報" }'
*/
Bundle getSkuDetails(int apiVersion, String appId, String licenceKey, in Bundle skusBundle);
/**
* ユーザーの購入済みサービス情報をリターン
* @param apiVersion アプリ内課金バージョン
* @param appId アプリID
* @param licenceKey ライセンスキー
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
*/
Bundle getPurchases(int apiVersion, String appId, String licenceKey);
/**
* 過去購入されたサービスを消費(consume)
* 一回以上同じサービスを購入する場合は必要
* @param apiVersion アプリ内課金バージョン
* @param appId アプリID
* @param licenceKey ライセンスキー
* @param purchaseId 決済ID
* @return 0=成功 、 その他=エラー
*/
int consumePurchase(int apiVersion, String appId, String licenceKey, String purchaseId);
}
必ず上記5つのメソッドを用意してください 。メソッドの引数・戻り値は【リファレンス】を参照してください。
appmartを利用するには下記permissionsを追加してください。
<!-- 課金API用 -->
<uses-permission android:name="jp.app_mart.permissions.APPMART_BILLING" />
少なくとも以下の処理がアプリ内で必要です。
1 IInAppBillingServiceにバインドします。 2 appmart アプリに IPCでbillingリクエストを送ります。 3 各billingリクエストごとに返ってくる同期レスポンスメッセージを処理します。
appmartのアプリ内課金サービスへの接続を確立するには、ActivityをInAppBillingV2Interfaceにバインドするための ServiceConnection を実装します。onServiceDisconnected と onServiceConnected メソッドをOverrideし、接続が確立された後に IInAppBillingServiceインスタンスへの参照を取得します。
InAppBillingV2Interface mService;
ServiceConnection mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = InAppBillingV2Interface.Stub.asInterface((IBinder) service);
}
};
ActivityのonCreateメソッド内で、bindServiceメソッドを呼んでバインドします。メソッドには アプリ内課金サービスを参照するIntentおよびServiceConnectionのインスタンスを渡します。
Intent serviceIntent = new Intent();
serviceIntent.setClassName("jp.app_mart", "jp.app_mart.service.AppmartInBillingV2Service");
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
mContext.bindService(serviceIntent, mServiceConn,Context.BIND_AUTO_CREATE);
} else {
if (listener != null) {
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
}
注意点:Activityが終了する場合、サービスからのアンバインドを忘れずに行ってください。
@Override
public void onDestroy() {
super.onDestroy();
if (mService != null){
unbindService(mServiceConn);
}
}
サービスが購入されると、ユーザーがそのサービスの所有権を取得したと認識し、そのサービスが消費されるまで同じプロダクトIDのサービスが購入されるのを防止します。アプリではサービスがどのように消費されるかをコントロールすることができ、appmartにそのサービスが再度購入できるようになったことを通知できます。また、ユーザーによって作成された購入リストをappmartから素早く取得することができます。例えば、ユーザーがアプリを起動したときにユーザーの購入リストをリストアしたときに便利です。
ユーザーの購入状況と関係なくappmartからサービスの詳細を問い合わせることができます。アプリ内課金V2サービスにリクエストを送るには、サービスIDの String ArrayList を作成し、それを "ITEM_ID_LIST" というキーで Bundle に保持します。
ArrayList<String> skuList = new ArrayList<String> ();
skuList.add("my_first_service_id");
skuList.add("my_second_service_id");
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(“ITEM_ID_LIST”, skuList);
appmart から情報を取得するには、アプリ内課金V2 のgetSkuDetails メソッドを呼び出します。第1引数には アプリ内課金 version の "2"、第2引数には呼び出しアプリのID、 第3引数には作成した Bundle を渡します。
// appmartにサービス情報を問い合わせる
Bundle skuDetails = mService.getSkuDetails(2, "application_id", "license_key", querySkus);
リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで 0 が含まれます。
値 | 備考 |
---|---|
0 | 成功 |
1 | ユーザーが[back]キーを押しました |
4 | 指定されたサービスイDが購入不可能 |
5 | 渡された引数が正しくありません |
6 | 致命的なエラーが発生 |
7 | 所有権あり |
8 | 所有権がないため、消費不可能 |
getSkuDetails メソッドをメインスレッドから呼ばないでください。このメソッドはネットワークリクエストのトリガーになり、メインスレッドをブロックします。
問い合わせた結果は、"DETAIL_LIST" というキーで String ArrayList に格納され、各購入情報は JSON 形式の文字列で格納されます。
int response = skuDetails.getInt("RESPONSE_CODE");
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");
// TODO UIを更新
}
}
json項目 | 備考 |
---|---|
productId | サービスID |
price | サービスの価格 (jpy) |
title | サービス名 |
description | サービス説明 |
アプリから購入リクエストを開始するには、getBuyIntentメソッドを呼び出します。 第1引数にはアプリ内課金versionの"2"、第2引数には呼び出しアプリID、第3引数にはサービス ID、第4引数にはアプリのライセンスキー、第4引数には developerPayload 文字列を渡します。 developerPayload 文字列は、購入情報としてappmartから返してほしい付加的な引数を指定するのに使います。
Bundle buyIntentBundle = mService.getBuyIntent(2, "application_id", "my_service_id", "license_key", "developer_pay_load");
リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで 0 が含まれます。また、"BUY_INTENT" というキーで取得できる PendingIntent で購入フローを開始出来ます。
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
購入処理を完了するには、取得したpendingIntentでstartIntentSenderForResultメソッドを呼び出します。
startIntentSenderForResult(pendingIntent.getIntentSender(),
1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
PendingIntentのレスポンスをアプリのonActivityResultに返します。onActivityResultメソッドはActivity.RESULT_OK (1) もしくは Activity.RESULT_CANCELED (0) を resultCode として持ちます。
値 | 備考 |
---|---|
RESPONSE_CODE | 0=成功 |
INAPP_PURCHASE_DATA | 購入情報をもつJSON文字列 |
INAPP_DATA_SIGNATURE | デベロッパーの公開鍵で暗号化された決済ID |
購入データは JSON 形式の文字列として Intent に格納されており、"INAPP_PURCHASE_DATA" というキーで取得できます。
{
"orderId":"1299976316905",
"packageName":"com.example.app",
"productId":"exampleSku",
"developerPayload":"developer_payload",
}
onActivityResultの実装
@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");
alert("You have bought the " + sku );
}
catch (JSONException e) {
alert("Failed to parse purchase data.");
e.printStackTrace();
}
}
}
}
セキュリティー向上のためdeveloperPayloadの文字列が以前に送った購入リクエストのものと一致するかチェックします。
ユーザーによる購入情報を取得するには、アプリ内課金V2のgetPurchasesメソッドを呼び出します。第1引数には アプリ内課金versionの "2"、第2引数には呼び出しアプリのID、第3引数にはライセンスキーを渡します。
Bundle ownedServices = mService.getPurchases(2, "application_id", "license_key");
appmartサービスは、デバイスに現在ログインしているユーザーアカウントの購入についてのみ返します。リクエストが成功した場合、返ってきた Bundle には RESPONSE_CODE というキーで 0 が含まれます。また、"INAPP_PURCHASE_ITEM_LIST" というキーで product ID のリストが、"INAPP_PURCHASE_DATA_LIST" というキーでオーダー詳細のリストが含まれます。
int response = ownedItems.getInt("RESPONSE_CODE");
if (response == 0) {
ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String sku = ownedSkus.get(i);
// do something with this purchase information
// e.g. display the updated list of products owned by user
}
// if continuationToken != null, call getPurchases again
// and pass in the token to retrieve more items
}
アプリ内課金V2を使って、appmartの購入されたサービスの所有権をトラックできます。一度サービスが購入されると、その所有権があると考えられappmartから購入できなくなります。appmart が再びサービスを購入可能にする前に、サービスの消費リクエストを送らなければなりません。
消費メカニズムをどう利用するかは開発者次第になります。 典型的には、ユーザーが複数回購入したいような一時的な利益があるプロダクト(例えばゲーム内通貨や装備)は消費可能な実装にします。一度だけ購入したり、永続的な効果を提供するプロダクト(例えばプレミアムアップグレード)は消費しないように実装します。
購入サービスの消費を記録するには、consumePurchaseメソッドを呼び出します。第1引数にはアプリ内課金versionの"2"、第2引数には呼び出しアプリのサービスID、第3引数にはライセンスキー、第4引数には決済IDを渡します。
int response = mService.consumePurchase(2, "my_first_service", "license_key", "9999999999999");
consumePurchase はメインスレッドから呼び出さないでください。このメソッドはネットワークリクエストのトリガーになり、メインスレッドをブロックします。
購入したサービスがユーザーにどのように提供されるかをコントロールしトラックするかは開発者の責任です。例えば、ゲーム内通貨をユーザーが購入した場合、購入した通貨の量に応じてプレイヤーの状態を変えなければなりません。
Security Recommendation: ユーザーにアプリ内課金を消費する利益をプロビジョニングする前に消費リクエストを送らなければなりません。サービスをプロビジョンする前にappmartから成功した消費レスポンスを受けとっていることを確認します。