Play Billing Library5を使う

# はじめに 

Play Billing Library5を使ってみます。
注意点として、本記事は私の独自の理解と解釈になります。 
課金処理周りの話になるので実装は自己責任でお願いします。

# Play Billing Libraryとは

AndroidアプリにおいてGoogle Playを通した課金処理を実装するために使用できるAPIです。毎年（？）更新されており、2022年5月からバージョン5.0がリリースされました。また、古いバージョンは時間が経つと足切りされてしまうので、定期的な更新が必要となります。

# 実装する
Play Billing Libraryを使用して販売できるアイテムには「消費可能なアイテム」とサブスクなどに使用される「消費不可のアイテム」があります。今回は「消費可能なアイテム」を例に実装の流れを見ていきます。 
Play Billing Libraryを使用して課金処理を行うために実装する必要がある処理は以下の7つです。 


1. 依存関係の追加（処理じゃないけど）
1. Billing Clientの初期化
1. Google Playへの接続
1. 購入可能なアイテムを表示
1. 購入フローの起動
1. 購入の処理
1. 未消費の購入の取得

### 1.依存関係の追加
まず、アプリのbuild.gradleファイルへ依存関係を追加します。 
-ktxも追加するとcoroutineを使用したメソッドを呼び出せるのでcoroutineを使用したい方は追加しましょう。本記事では基本的にcoroutineを使用したメソッドの方で説明しています。

```kotlin:build.gradle(app)
dependencies {
    val billing_version = "5.0.0"
    implementation("com.android.billingclient:billing:$billing_version")
    // coroutine使う場合は以下も追加
    implementation("com.android.billingclient:billing-ktx:$billing_version")
}
```

### 2.BillingClientの初期化 

BillingClientの初期化を行います。初期化する際にPurchaseUpdateListnerを渡す必要があります。これは、購入完了時などに購入の状態変更を取得するためのものになります。詳しくは「6.購入の処理」で後述します。  

```kotlin:BillingClientProvider.kt
val billingClient = BillingClient
            .newBuilder(context)
            .setListener(purchasesUpdatedListner) // 後述
            .enablePendingPurchases()
            .build()
```

### 3.Google Playへの接続  
Google Playへ接続を行います。接続時にセットしたBillingClientStateListnerに接続結果のコールバックが帰ってくるので結果に合わせてハンドリングします。接続に成功すると`billingClient.isReady`がtureを返すようになるので購入アイテムの表示やフローの起動をする際は確認してあげましょう。 

なお、`BillingClient.endConnection()`を呼んであげるとGoogle Playとの接続を終了させることができますが、一度接続を終了させた場合、billingClientは再度インスタンスを作り直さないと再接続できなくなってしまうので注意が必要です。

```kotlin:BillingClientProvider.kt
// 接続開始
fun startBillingConnection() {
    if (billingClient.isReady.not()) {
        billingClient.startConnection(object: BillingClientStateListener {

            override fun onBillingServiceDisconnected() {
                Timber.d("Billing Connection Disconnected")
                startBillingConnection() // 失敗したら再接続する
            }

            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    // 成功した時の処理
                } else {
                    // 問題発生時の処理
                }
            }
        })
    }
}

// 接続を終了させる
fun terminateBillingConnection() {
    billingClient.endConnection()
}
```

### 4. 購入可能なアイテムの表示  

購入可能なアイテムを取得します。購入可能なアイテムを取得するには実装以外の準備としていかが必要です。忘れないように準備しましょう。


- Google Play Console上でアプリを登録する
- Google Play Console上で登録したアプリ内で購入できるアイテムを登録する
- 登録したアイテムのアイテムIDを取得する

```kotlin:BillingClientProvider.kt
// 購入の詳細を取得
suspend fun queryProductDetail(): List<ProductDetails> {
    checkBillingClientStatus()
    val params = QueryProductDetailsParams.newBuilder()
    val productList = mutableListOf<QueryProductDetailsParams.Product>()
    val productIds = // アイテムIDのリスト（List<String>）
    for (id in productIds) {
        productList.add(
            QueryProductDetailsParams.Product.newBuilder()
                .setProductId(id)
                .setProductType(BillingClient.ProductType.INAPP) // 消費可能な購入なのでINAPPを指定、消費不可ならSUBSを指定
                .build()
        )
    }
    val result = billingClient.queryProductDetails(params.setProductList(productList).build())
    if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        val expectedProductDetailCount = BillingProduct.values().size
        result.productDetailsList?.let { productDetailsList ->
            if (productDetailsList.size == expectedProductDetailCount) {
                return productDetailsList // このListの中のProductDetailクラスから購入可能アイテムの詳細（値段など）を取得できる
            }
        }
        throw NotExpectedProductDetailCountException() // 取得したアイテム詳細の数が合わない
    } else {
        // 失敗時の処理
    }
}
```

### 5.購入フローの起動  

ユーザが購入するアイテムを決定したら購入フローを起動させます。購入フローの起動にはActivityが必要です。起動させると課金処理を行うダイアログが表示されるのでユーザは課金処理を行うことができます。課金処理を完了させたり購入フローをキャンセルすると、購入の状態が変化して「2.BillingClientの初期化」で登録したPurchaseUpdatedListnerで変更を取得できます。

```kotlin:BillingClientPurchaseFlowLauncher.kt
// 購入フローを開始
fun launchPurchaseFlow(activity: Activity, details: ProductDetails) {
    val params = BillingFlowParams.newBuilder().setProductDetailsParamsList(
        listOf(
            BillingFlowParams.ProductDetailsParams.newBuilder()
                .setProductDetails(details)
                .build()
        )
    ).build()
    if (billingClient.isReady.not()) { // 通信できないのでエラー返す }
    val billingResult = billingClient.launchBillingFlow(activity, params)
    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        // 失敗時の処理
    }
}
```

### 6.購入の処理  

ユーザが課金のダイアログを操作して購入を完了させると、購入の状態が変化して「2.BillingClientの初期化」で登録したPurchaseUpdatedListnerで変更を取得できます。実際にはPurchaseクラスという形でレシートに当たるものを取得できます。自前でサーバを持っている方はサーバへPurchaseから取得できる情報を送信して検証や承認を行い、その後購入を消費します。持っていない場合はAndroid端末上で検証するか、もしくはそのまま消費します。  


```kotlin:PurchaseUpdatedListnerImpl.kt

// PurchaseUpdatedListnerはinterfaceなので継承したクラスでonPurchaseUpdatedをoverrideしてやる
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.let {
                    // ここでPurchaseの配列が取得できるので、サーバに送ったり消費したりする
                } 
            }
        }
    }
}
```

```kotlin:BillingClientProvider.kt

// 購入の消費を行う、サーバ上での検証など終わったタイミングで呼び出す
suspend fun consumePurchase(purchase: Purchase) {
    checkBillingClientStatus()
    val consumeParams = ConsumeParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()
    val result = billingClient.consumePurchase(consumeParams)
    if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        // 成功時の処理
    } else {
        // 失敗時の処理
    }
}
```

### 7.未消費の購入の取得

最後に未消費の購入についてです。未消費の購入とは、ユーザによる課金が完了し購入が生成された後消費される前にアプリがキルされたなどの原因で消費されずに残っている購入となります。消費可能アイテムの場合、未消費の購入が残っていると同一のアイテムは未消費の購入が無くなるまで購入することができなくなります。また、消費を行うと自動的に承認も行われます。承認はサーバ上でも行えますが、承認が行われていない消費は3日経つとユーザへ払い戻しが行われてしまうので注意です。

Play Billing Libraryでは未消費の購入をGoogle Playから取得することができます。説明の関係上、最後に説明していますが実際はアプリを立ち上げてGoogle Playへの接続が完了した直後に行うとスムーズにハンドリングできるかもしれません。


```kotlin:BillingClientProvider.kt
// 未消費の購入を取得
override suspend fun queryPurchases(): List<Purchase> {
    if (billingClient.isReady.not()) throw BillingClientIsNotReadyException() // 未接続なら適当なExceptionをthrow
    val result = billingClient.queryPurchasesAsync(QueryPurchasesParams
            .newBuilder()
            .setProductType(BillingClient.ProductType.INAPP)
            .build()
        )
    
    if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        // resultから未消費のPurchaseを取得できるので消費またはサーバへ送る
    } else {
        // 失敗時の処理
    }
}
```

# 参考文献  

- [Google Play Billing Libraryをアプリに統合する](https://developer.android.com/google/play/billing/integrate?hl=ja)