Google Play Billing Library を使ってみる

#はじめに
Google Play Billing Library を使って、アプリ内課金を実装してみます。
このコードラボを参考にしてます。
https://github.com/googlecodelabs/play-billing-codelab

#課金の種類について
日常的に触れる課金は、以下のように分類されると思います。

 - 消費型商品
使う度に消費されるアプリ内通貨や、回復アイテムなど

 - 非消費型商品
広告削除、機能のアンロックなど、一度購入したら永続的に使えるもの

 - 定期購入型商品
開発者が指定した期間毎に自動的に購入されるもの
Netflixなどの月額課金などと呼ばれるもの

ちなみに `消費型` と `非消費型` と分けましたが、これらは厳密に分類されてるわけではなく、
購入した商品を消費するか、永続的に持つかどうかの実装の違いしかありません。

#実装
##ライブラリの導入
build.gradleのdependenciesに1行追加しましょう

```gradle
dependencies {
    ...
    implementation 'com.android.billingclient:billing:1.2.1'
}
```

##BillingClientのセットアップ
```kotlin
fun setup() {
  // ユーザーが商品を購入した時に呼ばれる
  val purchaseUpdateListener = object : PurchasesUpdatedListener {
    override fun onPurchasesUpdated(
        responseCode: Int,
        purchases: MutableList<Purchase>?
    ) {
      Log.d("onPurchasesUpdated", "responseCode:$responseCode")
      Log.d("onPurchasesUpdated", "purchase:${purchases?.toString()}")
      purchases?.forEach { purchase ->
        Log.d("onPurchasesUpdated", "purchase:$purchase")
      }
    }
  }

  // BillingClient : メインに使われるインターフェース
  val billingClient: BillingClient = BillingClient
      .newBuilder(Activity())
      .setListener(purchaseUpdateListener)
      .build()

  // セットアップ処理のコールバック
  val clientStateListener = object : BillingClientStateListener {
    override fun onBillingServiceDisconnected() {
      Log.d("onBillingServiceDisconnected", "Service disconnected.")
    }

    override fun onBillingSetupFinished(responseCode: Int) {
      if (responseCode == BillingClient.BillingResponse.OK) {
        Log.d("onBillingSetupFinished", "Setup success. responseCode:$responseCode")
      } else {
        Log.w("onBillingSetupFinished", "Setup error. responseCode:$responseCode")
      }
    }
  }

  // 接続開始
  billingClient.startConnection(clientStateListener)
}
```

##商品の詳細情報を取得
商品を購入するために、商品の詳細情報 `SkuDetails` が必要になります。
###querySkuDetailsAsync
`skuType` と `sku` を指定して詳細情報を非同期で取得出来ます。

```kotlin
// BillingClient.SkuType.INAPP : アプリ内通貨や、消費アイテムなど
val skuType = BillingClient.SkuType.INAPP
// SKU : 固有の商品 ID
val inAppSkus = listOf("gas", "premium")
val params = SkuDetailsParams.newBuilder()
      .setType(skuType)
      .setSkusList(inAppSkus)
      .build()

// 取得開始
billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
  if (responseCode == BillingClient.BillingResponse.OK) {
    Log.d(TAG, skuDetailsList.toString())
  }
}
```

こんな感じのデータが返ってきます

```
D/querySkuDetailsAsync:
[SkuDetails: {
  "productId": "gas",
  "type": "inapp",
  "price": "￥111",
  "price_amount_micros": 110686158,
  "price_currency_code": "JPY",
  "title": "Gas (Play Billing Codelab)",
  "description": "Buy gasoline to ride!"
},
SkuDetails: {
  "productId": "premium",
  "type": "inapp",
  "price": "￥166",
  "price_amount_micros": 166029237,
  "price_currency_code": "JPY",
  "title": "Upgrade your car (Play Billing Codelab)",
  "description": "Buy a premium outfit for your car!"
}]
```
以下、この SkuDetails を使って進めていきます。
##商品を購入

```kotlin
// 購入フローを開始するためのパラメータ
val billingFlowParams = BillingFlowParams.newBuilder()
      .setSkuDetails(skuDetails)
      .build()

// 購入フローを開始します
billingClient.launchBillingFlow(activity, billingFlowParams)
```
以下のように購入ダイアログが表示されます。
<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/301886/9f4c3715-7832-23d0-ed15-bef3acd72a07.gif">

購入後に `PurchasesUpdatedListener#onPurchasesUpdated()` が呼ばれます。

```
D/onPurchasesUpdated: responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json: // 購入した商品の情報が返ってくる
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium", // SKU
  "purchaseTime": ...,
  "purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
  "purchaseToken" ...:
}
```

追加でもう一度購入したいので、再び購入処理を行ってみると

``` 
W/BillingClient: Unable to buy item, Error response code: 7
```

エラーログが出てる…エラーコードを確認しましょう。

```BillingClient.BillingResponse
/** Failure to purchase since item is already owned */
int ITEM_ALREADY_OWNED = 7;
```

既に所有しているとのこと。

##商品を消費
商品を再び買うためには、購入済みの商品を `消費` しなければいけません。
`BillingClient#consumeAsync (String purchaseToken, ConsumeResponseListener listener)` を呼んで消費しましょう。
(※SkuType.SUBS の定期購入型商品は消費出来ません。)

###purchaseToken の取得
購入後に呼ばれる `onPurchasesUpdated` に `purchaseToken` 含む情報が返ってきてましたが、保存していなくても後から `BillingClient#queryPurchases()` で問い合わせる事が出来ます。

```kotlin
fun queryInAppPurchases() {
  // アプリ内で購入した指定 SkuType の商品の購入詳細を取得します
  val purchaseResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)

  // ログ確認
  Log.d("queryPurchases", "responseCode:${purchaseResult.responseCode}")
  Log.d("queryPurchases", purchaseResult.purchasesList.toString())
}
```

以下のようなデータが返ってきます。

```
D/queryPurchases: responseCode:0 // BillingClient.BillingResponse.OK
D/queryPurchases: Purchase. Json:
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium", // SKU
  "purchaseTime": ...,
  "purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
  "purchaseToken" ...:
}
```
もしアプリをアンインストールしてしまって、購入情報を失ってしまった場合も、購入時のGoogleアカウントが紐付いていれば、この情報を使って購入済みの状態を復元する事が出来そうです。

####purchaseToken を使って商品を消費する
```kotlin
fun consume(purchaseToken: String) {
  // 消費操作が終了した時に呼ばれるリスナー
  val consumeResponseListener = object : ConsumeResponseListener{
    override fun onConsumeResponse(responseCode: Int, purchaseToken: String?) {
      Log.d("onConsumeResponse", "responseCode:$responseCode")
      Log.d("onConsumeResponse", "purchaseToken:$purchaseToken")
    }
  }

  // purchaseToken とリスナーを渡して消費処理開始
  billingClient.consumeAsync(purchaseToken, consumeResponseListener)
}

```

消費ログを確認

```
D/onConsumeResponse: responseCode:0 // BillingClient.BillingResponse.OK
D/onConsumeResponse: purchaseToken:......
```

消費に成功してそうなので、もう一度 `BillingClient#queryPurchases()` で購入情報を確認してみましょう。

```
D/queryPurchases: responseCode:0
D/queryPurchases: []
```

リストが空になってますね。
購入してる商品が無い状態なので、購入処理を再度呼んでみます。

```
D/onPurchasesUpdated:responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json:
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium",
  "purchaseTime": ...,
  "purchaseState": ...,
  "purchaseToken" ...:
}
```

購入成功しました。
アプリ内課金の基本的な流れは以上となります。

##BillingClientをもう少し使ってみる
アプリ内課金のための便利な方法を提供してくれます。
他のメソッドも使ってみましょう。

###queryPurchaseHistoryAsync
購入が期限切れ、キャンセル、または消費された場合でも、ユーザーによって行われた最新の購入を返します。

```kotlin
fun queryPurchaseHistoryAsync() {
  // リスナーを用意
  val purchaseHistoryResponseListener = object :PurchaseHistoryResponseListener{
    override fun onPurchaseHistoryResponse(responseCode: Int,
        purchasesList: MutableList<Purchase>?) {
      Log.d(TAG, "responseCode:$responseCode")
      Log.d(TAG, purchasesList?.toString())
    }
  }

   // SkuTypeとリスナーを渡して呼び出し
   billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, purchaseHistoryResponseListener)
}
```

レスポンス

```
D/BillingManager: responseCode:0 // BillingClient.BillingResponse.OK
D/BillingManager: [Purchase.Json: {
	"productId": "gas",
	"purchaseToken": ......
	"purchaseTime": ......
	"developerPayload": ......
}, Purchase.Json: {
	"productId": "premium",
	"purchaseToken": ......
	"purchaseTime": ......
	"developerPayload": ......
}]
```

同じ商品を複数回買っていた場合、最新の購入情報が返ってきます。


###launchPriceChangeConfirmationFlow
ユーザが購読している商品の価格が変わったとき、このフローを起動してユーザに価格変更情報のある画面を表示させます。ユーザーは新しい価格を確認するか、またはフローをキャンセルすることができます。

※↓の処理の前に `sku == "gold_monthly"` の定期購読商品を定期購入しています。

```kotlin
fun launchPriceChangeConfirmationFlow() {
  // PriceChangeFlowParams の生成に SkuDetails が必要なので、取得します
  val skuType = BillingClient.SkuType.SUBS
  val subsSkus = listOf("gold_monthly", "gold_yearly")
  val skuDetailsParams = SkuDetailsParams.newBuilder()
      .setType(skuType)
      .setSkusList(subsSkus)
      .build()

  // SkuDetailsを取得する
  billingClient.querySkuDetailsAsync(skuDetailsParams) { responseCode, skuDetailsList ->
    // SkuType.SUBS の SkuDetailsList から SKU が gold_monthly の SkuDetails を取る
    val gold = skuDetailsList.filter { it.sku == "gold_monthly" }[0]
    // パラメータを生成する
    val priceChangeFlowParams = PriceChangeFlowParams.newBuilder()
        .setSkuDetails(gold)
        .build()

    // 価格変更確認処理を開始する
    billingClient.launchPriceChangeConfirmationFlow(activity, priceChangeFlowParams) {responseCode ->
      Log.d(TAG, "responseCode:$responseCode")
    }
  }
}
```

実際に価格変更は試していませんが、
価格の変更をユーザーに知らせて、契約更新を選べるんでしょうか。
<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/301886/d5ca1d92-faa8-909a-db83-309df4bb8251.gif">

##関連リンク
https://developer.android.com/google/play/billing/billing_library_overview
https://developer.android.com/google/play/billing/billing_onetime
https://github.com/googlecodelabs/play-billing-codelab


以上です。