Androidアプリの課金周り(IAB v3)についてまとめてみた

Androidアプリでの課金周り(IAB v3)にについてまとめてみました。
ただし、レシート検証などセキュリティ周りについては触れてないのでその点は注意が必要です。


# 事前準備
IABは、端末にインストールされているGoogle Playアプリとやりとりしながら課金サービスにアクセスする。
そのため、GooglePlayアプリとやりとりするためのモジュールを追加したり、Google Consoleに各種データを登録する必要があったりと事前準備が色々と必要になる。

## Google Consoleへの登録
IABを利用するためにはGoogle Consoleに各種情報を登録する必要がある。
必要なのは以下の4ステップ。

 - 1) apkのアップロード
 - 2) 各種データの登録
 - 3) アプリ内アイテムの登録
 - 4) テスターアカウントの登録

#### 1) apkのアップロード
 - Google Consoleで「新しいアプリを追加」をし、新たにアプリを追加する。
 - メニューの「APK」から「アルファ版テスト」か「ベータ版テスト」を選択し、署名付きのapkファイルをアップロードする。
   - IABのテストのためなので、製品版ではなくアルファ版かベータ版を選ぶ。

apkファイルをアップロードする上での注意点は以下。

```注意点
・アルファ版のテスターが使用できるようにするには、アルファ版 APK のバージョンコードをベータ版 APK より高くする必要があります。
・ベータ版 APK をアルファ版 APK より高いバージョンコードでアップロードした場合は、アルファ版が自動的に無効化されます。
・製品版 APK をアルファ版またはベータ版 APK より高いバージョンコードでアップロードした場合は、アルファ版やベータ版が自動的に無効化されます。

https://support.google.com/googleplay/android-developer/answer/3131213?hl=ja
```

バージョンコードについての情報は[本ページ](http://qiita.com/tarappo/items/6d20d2f4bc03135ed9c7)を参照



#### 2) 各種データの登録
 - IABを利用するためには、「ストアの掲載情報」を登録する必要があるので、登録する。
   - タイトルや説明だけでなく、各種スクリーンショットも登録する。

登録している情報が足りなければ、説明が表示されるのでそれに従って登録を行っていくのが良い。


#### 3) アプリ内アイテムの登録
IABで利用する「アプリ内アイテム」を登録する。

アイテムは、以下の2種類が存在する。

 - 管理対象のアイテム
 - 定期購入のアイテム

以前は、管理対象外と管理対象と分かれていたがIAB v3で1つになった。
そのため、消費させるタイプのアイテムにしたい場合は、アプリ側で毎回消費という行為をする必要がある。
この点は、AppleのIAPとは異なるので注意が必要。


#### 4) テスターアカウントの登録
IABを利用するテスターを登録する必要がある。
ここがやや面倒なので、注意が必要。

最初に

 - 「APK」から1)でapkアップロード時に選択したテストを選択する。
   - 「ベータ版テスト」または「アルファ版テスト」

テスト方法として以下の2種類がある。

 - クローズド：指定した個別のユーザーが対象のテスト
 - オープン：所定のリンクを知っていれば参加できる一般公開テスト


クローズドの場合はさらに以下の手順が必要

 - 対象となるテスターを登録する。
   - 登録後、反映されるまでに時間がかかる点に注意が必要。
 - オプトインURLが表示されているので、これをテスター側の端末でアクセスする。


### 端末側の準備
端末側では以下の2点を行っておく必要がある。

 - 作成したテスターアカウントで、GooglePlayアプリにログインしておく。
 - オプトインURLにアクセスし、「テスターになる」をクリックしておく。


## 実装前準備
### AndroidManifestにパーミッション設定追加
IABを利用するために以下のパーミッションを追加する。

```AndroidManifest.xml
<uses-permission android:name="com.android.vending.BILLING" />
```

### 決済モジュールの追加
Android SDK Managerを起動し、ExtrasにあるGoogle Play Billing Libraryをインストールする。

![スクリーンショット 2015-11-09 9.38.34.png](https://qiita-image-store.s3.amazonaws.com/0/37642/57942525-0172-2e14-7b06-ac6dd30bb271.png "スクリーンショット 2015-11-09 9.38.34.png")


インストール後に、以下の場所にaidlファイルがあることを確認する。

```
[android_sdk_path]/extras/google/play_billing/IInAppBillingService.aidl
```

上記のaidlファイルを、自身のプロジェクトの以下に配置する。

```
/app/scr/main/java/com/android/vending/billing/IInAppBillingService.aidl
```
※上記は、AnroidStudioで作られたデフォルト構成を元にしています。

AndroidStudioでは以下のように表示される。

![スクリーンショット 2015-11-20 9.36.12.png](https://qiita-image-store.s3.amazonaws.com/0/37642/45275ddb-3952-f36d-97c9-b20e4f1bdcbe.png "スクリーンショット 2015-11-20 9.36.12.png")


### IInAppBillingServiceへの接続
ActivityのonCreate時に課金サービスを開始し、onDestory時に課金サービスを終了するようにする。


```java:サンプルコード
IInAppBillingService billingService;

ServiceConnection mServiceConn = new ServiceConnection() {
	@Override
    public void onServiceDisconnected(ComponentName name) {
    	billingService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
    	billingService = IInAppBillingService.Stub.asInterface(service);
    }
};

@Override
protected 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);
}


@Override
public void onDestroy()  {
	super.onDestroy();

    // 課金サービスを終了
    if(billingService != null){
    	unbindService(mServiceConn);
    }
}
``` 


## IABにおける各処理
上記にある前準備でGoogle Playに接続し、IABを利用することができるようになっている。

以下にそれぞれのサンプルコードを書いていますが、以下のコードだけではレシート検証については一切ふれておらず、セキュリティ的には不十分です。

### 基本的なシーケンス
購入時のシーケンスは以下の様な感じになる。

 - isBillingSupported
 - getPurchases
 - consumePurchases
 - getSkuDetails
 - getBuyIntent
 - startIntentSenderForResult

このシーケンスにしたがって以下にサンプルコードを記述しておく。

#### isBillingSupported
 - 端末がアプリIABをサポートしているかどうか。

```java:サンプルコード
try {
	int result = billingService.isBillingSupported(3, getPackageName(), "inapp");    
	if(result == 
} catch (RemoteException e) {
	e.printStackTrace();
}
```

#### getPurchases
 - GooglePlayにログインしているアカウントで所持しているBundle(product IDsを含む)を取得する。

購入後に消耗していなければ、所持している状態になっている。


```java:サンプルコード
try {
	Bundle ownedItems = billingService.getPurchases(3, getPackageName(), "inapp", null);

  　 //失敗した場合
    if (ownedItems.getInt("RESPONSE_CODE") != 0) {
    	return;
    }

    ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");

	//取得したデータの情報
    for(String purchaseData : purchaseDataList){
    	//消耗型アイテムであれば消費処理をおこなう
    }
} catch (RemoteException e) {
	e.printStackTrace();
}
```

上記コードでは、1種類のみ使ってデータを取得しているが以下の4種類が存在する。

 - INAPP_PURCHASE_ITEM_LIST
   - productIdsのリスト
 - INAPP_PURCHASE_DATA_LIST
   - 詳細情報の一覧(orderIdやpackageName、productIdなどを含む)
 - INAPP_DATA_SIGNATURE_LIST
   - 購入情報のシグネチャ(デベロッパーの秘密鍵をつかって生成されている)
 - INAPP_CONTINUATION_TOKEN
   - 大量のBundleがある場合は、1度に全ての情報を取得できない。そのため、このキーで得た値をgetPurchasesの引数として渡すことにより、続きのデータを取得することが出来る。



#### consumePurchases
 - アイテムの消費をおこなう。

購入したアイテムの消費をどのようなタイミングでおこなうかは開発者側に委ねられている。
そのため、消耗型にしたい場合はそのアイテムに対して消費リクエスト(consumePurchases)を送る必要がある。

```java:サンプルコード
try {
	Bundle ownedItems = billingService.getPurchases(3, getPackageName(), "inapp", null);

    ArrayList<String> purchaseDataList = ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   
    for(String purchaseData : purchaseDataList){
   		JSONObject obj = new JSONObject(purchaseData);
       String purchaseToken = obj.optString("token", obj.optString("purchaseToken"));

       billingService.consumePurchase(3, getPackageName(), purchaseToken);
    }
}catch(Exception e) {
	e.printStackTrace();
}
```

本リクエストはメインスレッドをブロックするため、メインスレッドで呼ばないようにするべき。


#### getSkuDetails
 - 購入可能なアイテムの詳細情報の取得。

取得は1つではなく、複数取得することが出来る。

```java:サンプルコード
try {
	ArrayList<String> itemList = new ArrayList<String>();
    itemList.add(skuName);

    Bundle skusBundle = new Bundle();
    skusBundle.putStringArrayList("ITEM_ID_LIST", itemList);

    Bundle details = billingService.getSkuDetails(3, getPackageName(), "inapp", skusBundle);

    int responseCode = details.getInt("RESPONSE_CODE");
    ArrayList<String> responseList = details.getStringArrayList("DETAILS_LIST");
    
} catch (RemoteException e) {
    e.printStackTrace();
}
```        

詳細情報に関しては以下の値が返ってくる。

 - productId
   - 指定したsku
 - type
   - inapp or subs(定期購買)
 - price
   - 登録した値段
   - 例："JPY120"
 - price_amount_micros
   - 例：120000000
 - price_currency_code(ISO 4217)
   - 例："JPY"
 - title
   - 登録した名前
 - description
   - 登録した説明

基本的に、Google Consoleで登録した情報が返ってくる。


#### getBuyIntent / startIntentSenderForResult
 - アイテムを購入する。

```java:サンプルコード
try {
	Bundle buyIntentBundle  = billingService.getBuyIntent(3, getPackageName(), skuName, "inapp", "you need write this developerPayload");

    PendingIntent buyIntent = buyIntentBundle.getParcelable("BUY_INTENT");

    MainActivity.this.startIntentSenderForResult(buyIntent.getIntentSender(), requestCode, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}catch (Exception e) {
	e.printStackTrace();
}
```
developerPayload文字列は、GooglePlayから返してほしい付加的な引数を指定するのに利用する。
セキュリティ的なことを考える場合、ユニークな文字列を指定し、返ってきた情報を元にチェックすることが望ましい。
※この文字列に各々のサービスのユーザIDを埋め込むケースが多い。

PendingIntentのレスポンスをonActivityResultに返し、購入データはIntentに格納されている。

```java:サンプルコード
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)  {
	//startIntentSenderForResultで指定したrequestCodeと一緒であることの確認
	if(requestCode == this.requestCode){
    	if(resultCode == RESULT_OK){
       }else {
       }
    }
}
```
resultCodeには、RESULT_OK(1)もしくはRESULT_CANCELED(0)が返ってくる。


Intentから以下のキーで情報を取得することが出来る。

 - RESPONSE_CODE
   - 成功時は0
 - INAPP_PURCHASE_DATA
   - 購入データ
 - INAPP_DATA_SIGNATURE
   - 購入情報のシグネチャ(デベロッパーの秘密鍵をつかって生成されている)

INAPP_PURCHASE_DATAとINAPP_DATA_SIGNATUREで得られた情報を元に、サーバ側で不正購入のチェックをするのが望ましい。


##### INAPP_PURCHASE_DATAで返ってくるJSON fieldsの詳細

 - autoRenewing
    - 定期購入用(自動購読状態かのフラグ) 
 - orderId
    - Google WalletのorderIDと同じ
 - packageName
    - 購入したアプリのpackageName
 - productId
    - 購入したアイテム(sku)
 - purchaseTime
    - 購入時間
 - purchaseState
    - 購入状態：0なら購入完了、1ならキャンセル、2は払い戻し
 - developerPayload
    - getBuyIntent時に指定したdeveloperPayload
 - purchaseToken
    - 購入時に発行されるトークン



## 参考資料
 - http://developer.android.com/google/play/billing/api.html
 - http://developer.android.com/google/play/billing/billing_integrate.html
 - http://developer.android.com/google/play/billing/billing_reference.html
 - https://developer.android.com/google/play/billing/gp-purchase-status-api.html
 - https://developer.android.com/google/play/billing/billing_best_practices.html