AndroidでGoogle Play月額課金を実装する

最近やる機会のあった、Androidのアプリ内課金（月額課金）の実装のメモ。

## ライブラリの設定
まずは実装に必要なGoogle Play Billing Libraryを追加。

```groovy:app/build.gradle
compile 'com.android.billingclient:billing:1.0'
```

Googleで調べたりすると、`IInAppBillingService`をプロジェクトに追加してゴニョゴニョ…ってやり方がよく出てくるが、最近は上記のライブラリを使えば良さそう。

パーミッションを`AndroidManifest.xml`に追加する必要もない。

## アプリ内購入機能への接続開始・終了
基本的に`BillingClient`クラスを利用して処理を書いていく。

`startConnection`でクライアントのセットアップ・サービスへの接続を開始し、その引数の`BillingClientStateListener`の`onBillingServiceDisconnected`と`onBillingSetupFinished`でその結果を受け取ることが可能。後者の引数には、整数のレスポンスコード（`BillingClient.BillingResponse`）が入っており、その値に応じて処理を書いていくことになる。

```kotlin:BillingActivity.kt
class BillingActivity : AppCompatActivity(), PurchasesUpdatedListener {
    private lateinit var billingClient: BillingClient

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        billingClient = BillingClient.newBuilder(this).setListener(this).build()
        startServiceConnection(null)
    }

    override fun onDestroy() {
        super.onDestroy()
        billingClient.endConnection()
    }

    private fun startServiceConnection(executeOnSuccess: Runnable?) {
        if (billingClient.isReady) {
            executeOnSuccess?.run()
            return
        }
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingServiceDisconnected() {}

            override fun onBillingSetupFinished(@BillingClient.BillingResponse responseCode: Int) {
                when (responseCode) {
                    BillingClient.BillingResponse.OK -> executeOnSuccess?.run()
                    else -> {}
                }
            }
        })
    }

    // PurchasesUpdatedListener
    override fun onPurchasesUpdated(@BillingClient.BillingResponse responseCode: Int, purchases: MutableList<Purchase>?) {
    }
}
```

billingClientの`setListener`の引数は`PurchasesUpdatedListener`で、ユーザがアイテムを購入したときなどに呼ばれる`onPurchasesUpdated`を提供する。こちらの引数も`BillingClient.BillingResponse`。

サービスへの接続を終了する際は、`endConnection`を利用。アクティビティのライフサイクルに合わせて、`onDestroy`などで実行する。

## 購入可能なアイテムの取得
`BillingClient`を通して、Google Playコンソールで作成したアプリ内で購入できるアイテムを非同期に取得することができる。情報取得に必要なパラメータ`SkuDetailsParams`に購入アイテムのIDや課金タイプ（今回は月額課金）を指定して、`querySkuDetailsAsync`で取得開始。このメソッドの2番目の引数は`SkuDetailsResponseListener`となっており、アイテムの情報取得結果が通知されるリスナになっている。

```kotlin:BillingActivity.kt
class BillingActivity : AppCompatActivity(), PurchasesUpdatedListener, SkuDetailsResponseListener {

    private fun retrieveSubscriptionItems() {
        val executeOnConnectedService = Runnable {
            val params: SkuDetailsParams.Builder = SkuDetailsParams.newBuilder()
                    .setSkusList(listOf("itemA", "itemB"))
                    .setType(BillingClient.SkuType.SUBS)
            billingClient.querySkuDetailsAsync(params.build(), this)
        }
        startServiceConnection(executeOnConnectedService)
    }

    // SkuDetailsResponseListener
    override fun onSkuDetailsResponse(@BillingClient.BillingResponse responseCode: Int, skuDetailsList: MutableList<SkuDetails>?) {
        when (responseCode) {
            BillingClient.BillingResponse.OK -> {
                if (skuDetailsList != null) {
                    skuDetailsList.forEach { ... }
                    return
                }
            }
            else -> {}
        }
    }
}
```

正常に取得できた際に受け取れる`SkuDetails`クラスを通して、各アイテムの価格や説明などを得ることができる。

## 購入
購入にはbillingClientの`launchBillingFlow`を利用する。
購入に必要なパラメータは`BillingFlowParams`が提供している。アイテム名や購入タイプを指定するほか、ユーザを一意に識別する文字列やアップグレード/ダウングレード時の請求のオプションなどを指定する。
また、既に別のアイテムを購入済で、新しい購入の扱いがダウングレード/アップグレードとなる場合、`addOldSku`で購入済のアイテムのIDを指定しておかないといけない。

```kotlin:BillingActivity.kt
class BillingActivity : AppCompatActivity(), PurchasesUpdatedListener, SkuDetailsResponseListener {

   override fun startBillingFlow() {
        val executeOnConnectedService = Runnable {
            val billingFlowParamsBuilder = BillingFlowParams.newBuilder()
                    .setSku("itemA")
                    .setAccountId("XXXXXXXXXXXXXXX")
                    .setReplaceSkusProration(true)
                    .setType(BillingClient.SkuType.SUBS)
                    .setVrPurchaseFlow(false)
            billingClient.launchBillingFlow(this, billingFlowParamsBuilder.build())
        }
        startServiceConnection(executeOnConnectedService)
    }

    // PurchasesUpdatedListener
    override fun onPurchasesUpdated(@BillingClient.BillingResponse responseCode: Int, purchases: MutableList<Purchase>?) {
        when (responseCode) {
            BillingClient.BillingResponse.OK -> {
                if (purchases != null && purchases.isNotEmpty()) {
                    ....
                }
            }
            BillingClient.BillingResponse.FEATURE_NOT_SUPPORTED -> {}
            BillingClient.BillingResponse.ITEM_ALREADY_OWNED -> {}
            BillingClient.BillingResponse.USER_CANCELED -> {}
            else -> {}
        }
    }
}
```

最初に書いたとおり、購入の結果は`onPurchasesUpdated`に通知され、レスポンスに応じて処理を分けていく。

## 購入済のアイテムの取得
購入済のアイテムを取得する。Google Playストアアプリのキャッシュを利用するため、ネットワークを介さずに同期的に取得可能。

```kotlin:BillingActivity.kt
val purchasesList: List<Purchase> = billingClient.queryPurchases(BillingClient.SkuType.SUBS).purchasesList
```

## 参考
- [Android Developers Blog: Google Play Billing Library 1.0 released](https://android-developers.googleblog.com/2017/09/google-play-billing-library-10-released.html)
- [In-app Billing Overview | Android Developers](https://developer.android.com/google/play/billing/billing_overview.html)
- [Play Billing Library | Android Developers](https://developer.android.com/google/play/billing/billing_library.html)
- [Buy and Subscribe: Monetize your app on Google Play](https://codelabs.developers.google.com/codelabs/play-billing-codelab/#0)

## [PR] 仲間探してます
アトラエで一緒にネイティブアプリやWeアプリを書いてくれる仲間を募集中です！
https://www.green-japan.com/company/172