サブスク始めてみませんか？Google Play Billing Libraryの簡単なまとめ

# はじめに

自分が開発しているAndroidアプリに、アプリ内購入機能を実装したい衝動に駆られました。
Androidアプリにアプリ内購入機能を実装するためには、Google Play Billing Libraryを使ってロジックを実装していく必要があります。

しかし、これがなかなか複雑で難しい。

サンプルコードをみてみても、どこが出発点でどこまでが前処理で、どこからが実際の購入フローなのかよくわからない箇所が多くありました。

そこで今回は、自分のためにGoogle Play Billing Libraryを使って課金機能を実装するのに必要な各クラスの意味やインターフェースについての解説を簡単にまとめてみようと思います。

Google Play Billing Libraryを用いた具体的な実装方法については、以下のコードラボとリポジトリを参照してください。この記事と照らし合わせながら読んでいくと理解が深まると思います。

https://codelabs.developers.google.com/play-billing-codelab#0

https://github.com/android/play-billing-samples/tree/main/PlayBillingCodelab

# ProductDetailsとPurchase

Google Play Billing Libraryで行う処理は、`ProductDetails`と`Purchase`の内容に強く依存します。これらのデータを取得してから、それらの状態に応じて必要な処理を行なっていくのが最初に実装が必要な部分になります。

ここでは、`ProductDetails`と`Purhcase`の存在理由やプロパティについて簡単に解説を行います。

## ProductDetails

購入を行うために必要なのが、商品もしくは定期購入を表す`ProductDetails`です。

`getProductId()`を通してGoogle Play Consoleで設定した商品IDを取得することができます。

`getProductType()`を通して、`BillingClient.ProductType`を取得することができます。値として、アプリ内商品を表す`INAPP`かアプリのサブスクリプションを表す`SUBS`が格納されています。

`getSubscriptionOfferDetails()`を通して、ユーザーが対象となるオファーを取得することができます。オファーとは、定期購入商品を購入するために利用可能な購入プランのことを言います。

https://developer.android.com/reference/com/android/billingclient/api/ProductDetails

## Purchase

ユーザーによるアプリ内課金での購入履歴を表すのが`Purchase`です。

`getPurchaseToken()`を通して、購入トークンを取得することができます。この値は`BillingClient`の`acknowledgePurchase()`を呼び出す際に必要な`AcknowledgePurchaseParams`の生成に必要な値です。

`isAcknowledged()`を通して、この購入は承認済みかどうかを確認することができます。

https://developer.android.com/reference/com/android/billingclient/api/Purchase

# ParamsとParams.Builder

Google Play Billing Libraryを使ってアプリ内課金のロジックを実装していると、至る所に`**Params`や`Builder`といった単語に出会します。そして、サンプルコード名をみても、すべての変数名が`params`になっていて、何の`params`を表しているのか初見ではわかりにくい箇所が多数存在します。

ここでは、主な`Params`とそれを取得するために必要な`Params.Builder`について簡単な解説を行います。

## QueryProductDetailsParams

`QueryProductDetailsParams`のインスタンスは、`QueryProductDetailsParams.Builder`のインスタンスに対して`build()`を呼び出すことで取得します。

```kotlin
val productDetailsParamsBuilder = QueryProductDetailsParams.newBuilder()
val productList = mutableListOf<QueryProductDetailsParams.Product>()

productDetailsParamsBuilder.setProductList(productList)
val productDetailsParams = productDetailsParamsBuilder.build()
```

## QueryProductDetailsParams.Product

`QueryProductDetailsParams.Product`のインスタンスは、`QueryProductDetailsParams.Product.Builder`のインスタンスに対して`build()`を呼び出すことで取得します。

```kotlin
val product = "subscription-product-id"
val queryProductDetailsParamsProduct = QueryProductDetailsParams.Product
    .newBuilder()
    .setProductId(product)
    .setProductType(BillingClient.ProductType.SUBS)
    .build()
```

## QueryPurchasesParams

`QueryPurchasesParams`のインスタンスは、`QueryPurchasesParams.Builder`のインスタンスに対して`build()`を呼び出すことで取得します。

```kotlin
val queryPurchasesParams = QueryPurchasesParams
    .newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()
```

## BillingFlowParams

`BillingFlowParams`のインスタンスは、`BillingFlowParams.Builder`のインスタンスに対して`build()`を呼び出すことで取得します。

```kotlin
BillingFlowParams
  .newBuilder()
  .setProductDetailsParamList(
    BillingFlowParams.ProductDetailsParams
      .newBuilder()
      .setProductDetails(productDetails)
      .setOfferToken(offerToken)
      .build()
  )
```

このときに指定する`offerToken`は`List<ProductDetails.SubscriptionOfferDetails>`から取得します。

```kotlin
val offerDetails: List<ProductDetails.SubscriptionOfferDetails> = ...

for (offer in offerDetails) {
  val offerToken = offer.offerToken
  Log.d(TAG, "offerToken: $offerToken")
}
```

サブスクリプションのアップグレードおよびダウングレードを行う場合は、`build()`を呼び出す前に`setSubscriptionUpdateParams()`を呼び出します。

このときに指定する`oldToken`は、アップグレードまたはダウングレードをされる側の購入トークンを表します。

```kotlin
BillingFlowParams
  .newBuilder()
  .setProductDetailsParamsList(
    listOf(
        BillingFlowParams.ProductDetailsParams
          .newBuilder()
          .setProductDetails(productDetails)
          .setOfferToken(offerToken)
          .build()
    )
  )
  .setSubscriptionUpdateParams(
    BillingFlowParams.SubscriptionUpdateParams
      .newBuilder()
      .setOldPurchaseToken(oldToken)
      .setReplaceProrationMode(
        BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
      )
      .build()
  )
  .build()
```

## BillingFlowParams.ProductDetailsParams

`BillingFlowParams.ProductDetailsParams`のインスタンスは、`BillingFlowParams.ProductDetailsParams.Builder`のインスタンスに対して、`build()`を呼び出すことで取得します。

```kotlin
BillingFlowParams.ProductDetailsParams
    .newBuilder()
    .setProductDetails(productDetails)
    .setOfferToken(offerToken)
    .build()
```

## AcknowledgePurchaseParams

`AcknowledgePurchaseParams`のインスタンスは、`AcknowledgePurchaseParams.Builder`のインスタンスに対して`build()`を呼び出すことで取得します。

```kotlin
val acknowledgePurchaseParams = AcknowledgePurchaseParams
    .newBuilder()
    .setPurchaseToken(purchase.purchaseToken)
    .build()
```

# BillingClient

`BillingClient`は、購入に関する通信を行うために使用されるという中心的な役割を果たします。
このクラスのインスタンスもまた、`newBuilder()`を使ってビルダーを取得し、それに対して`build()`を呼び出すことでインスタンスを取得するという流れになっています。

## 初期化と終了

`BillingClient`は一度しか使用できません。そのため、`onDestroy()`が呼び出されてGoogle Play Storeへの接続が終了し、`onCreate()`が呼び出されるたびに新しいインスタンスを作成する必要があります。

```kotlin
private lateinit var billingClient: BillingClient

// ...

billingClient = BillingClient.newBuilder(applicationContext)
    .setListener(
        object: PurchasesUpdatedListener {
            override fun onPurchasesUpdated(
                billingResult: BillingResult, 
                purchases: MutableList<Purchase>?
            ) {
                // TODO: ...
            }
        }
    )
    .enablePendingPurchases() // サブスクリプションには使用されません。
    .build()
if (!billingClient.isReady) {
    Log.d(TAG, "BillingClient: 接続を開始...")
    billingClient.startConnection(
        object : BillingClientStateListener {
            override fun onBillingSetupFinished(
                billingResult: BillingResult
            ) {
                // TODO: ...
            }

            override fun onBillingServiceDisconnected() {
                startBillingConnection(
                    /** BillingClienstStateListenerを実装したクラス **/
                )
            }
        }
    )
}

// ...

if (billingClient.isReady) {
    Log.d(TAG, "BillingClient: 接続を終了...")
    billingClient.endConnection()
}
```

以下では、このクラスに実装されている便利なメソッドについて簡単に解説していきます。

## setListener()

`setListener()`に渡すのは、`PurchasesUpdatedListener`を実装したクラス。
このメソッドを呼び出す理由は、すべてのアプリ内購入に関する最新のデータを渡したクラスで受信するためです。

## startConnection()

`startConnection()`に渡すのは、`BillingClientStateListener`を実装したクラス。

Google Playとの接続を確立するために、このメソッドは呼び出されます。接続の準備ができ次第、渡したクラスのメソッドが呼び出されます。

## queryProductDetailsAsync()

アプリで提供されている購入可能な商品の情報を取得するために、このメソッドは呼び出されます。

ここでは、`ClassyTaxiAppKotlin`のコードを参考に、`queryProductDetailsAsync()`を呼び出すまでの手順を見ていきます。

https://github.com/android/play-billing-samples/tree/main/ClassyTaxiAppKotlin

```kotlin
private fun queryProductDetails() {

    val productList: MutableList<QueryProductDetailsParams.Product> = arrayListOf()

    // LIST_OF_PRODUCTSは商品IDのリスト
    for (product in LIST_OF_PRODUCTS) {
        // QueryProductDetailsParams.Productのインスタンスを作成してリストに追加する
        productList.add(
            QueryProductDetailsParams.Product.newBuilder()
                .setProductId(product)
                .setProductType(BillingClient.ProductType.SUBS)
                .build()
        )
    }
    //  QueryProductDetailsParams.Builderを準備する
    val params = QueryProductDetailsParams.newBuilder()
    // QueryProductDetailsParams.BuilderにQueryProductDetailsParams.Productの配列をセットする。
    // paramsとproductDetailsParamsは同じ値を指す（変数名が違うため混同しないように注意）
    params.setProductList(productList).let { productDetailsParams ->
        billingClient.queryProductDetailsAsync(
            productDetailsParams.build(), 
            object : ProductDetailsResponseListener { 
                override fun onProductDetailsResponse(
                    billingResult: BillingResult,
                    productDetailsList: MutableList<ProductDetails>
                ) {
                    // TODO: 
                }
             }
        )
    }
}
```

それでは、ロジックの詳細を確認していきます。

### QueryProductDetailsParams.Productを準備する

商品IDのリストを準備します。

```kotlin
val productList: MutableList<QueryProductDetailsParams.Product> = arrayListOf()
```

`QueryProductDetailsParams.Product.newBuilder()`を呼び出して、`QueryProductDetailsParams.Product.Builder`を取得します。

```kotlin
QueryProductDetailsParams.Product.newBuilder()
```

それに対して、`setProductId()`で商品IDをセットします。

```kotlin
QueryProductDetailsParams.Product.newBuilder()
    .setProductId(product)
```

それに対して、`setProductType()`を呼び出して、商品タイプを設定します。

```kotlin
QueryProductDetailsParams.Product.newBuilder()
    .setProductId(product)
    .setProductType(BillingClient.ProductType.SUBS)
```

それに対して、`build()`を呼び出して、`QueryProductDetailsParams.Product`を取得します

```kotlin
QueryProductDetailsParams.Product.newBuilder()
    .setProductId(product)
    .setProductType(BillingClient.ProductType.SUBS)
    .build()
```

これを用意した商品IDの個数分繰り返して、作成された`QueryProductDetailsParams.Product`が格納されたリストを作成します。

```kotlin
for (product in LIST_OF_PRODUCTS) {
    productList.add(
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId(product)
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    )
}
```

### QueryProductDetailsParamsを準備する

`QueryProductDetailsParams.newBuilder()`を呼び出して、`QueryProductDetailsParams.Builder`を取得します。

```kotlin
val params = QueryProductDetailsParams.newBuilder()
```

`QueryProductDetailsParams.Builder#setProductList()`を呼び出して、上記で作成した`QueryProductDetailsParams.Product`が格納されたリストをセットします。

```kotlin
val params = QueryProductDetailsParams.newBuilder()
params.setProductList(productList)
```

それに対して、`build()`を呼び出して`QueryProductDetailsParams`のインスタンスを取得します。

```kotlin
val params = QueryProductDetailsParams.newBuilder()
params.setProductList(productList)
params.build()
```

### 用意したparamsを渡してqueryProductDetailsAsync()を呼び出す

`BillingClient#queryProductDetailsAsync()`に`QueryProductDetailsParams`のインスタンスと`ProductDetailsResponseListener`を実装したクラスを渡して呼び出します。

```kotlin
billingClient.queryProductDetailsAsync(params.build(), object : ProductDetailsResponseListener {
    override fun onProductDetailsResponse(
        billingResult: BillingResult,
        productDetails: MutableList<ProductDetails>
    ) {
        // TODO: ...
    }
})
```

## queryPurchasesAsync()

`queryPurchasesAsync()`を呼び出すことでGoogle Play Billingに対して、既存の購入履歴をリクエストします。

新規購入の情報については`PurchasesUpdatedListener`を実装したクラスに渡されることになっています。このメソッドの役割の詳細については、以下の章で解説しています。

購入トークンがいつ削除されるかを知りたい場合は、Google Play Billing APIを通して確認してください。

第一引数には、`QueryPurchasesParams`のインスタンスを渡します。
第二引数には、`PurchasesResponseListener`を実装したクラスを渡します。

```kotlin
billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build(),
    object: PurchasesResponseListener {
        override fun onQueryPurchasesResponse(
            p0: BillingResult,
            p1: MutableList<Purchase>
        ) {
            TODO("Not yet implemented")
        }
    }
)
```

## launchBillingFlow()

現在のアクティビティに基づいて購入フローを実行するために`launchBillingFlow()`を呼び出します。

第一引数には、`Activity`型の値を渡します。
第二引数には、`BillingFlowParams`のインスタンスを渡します。

```kotlin
if (!billingClient.isReady) {
    Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
}
billingClient.launchBillingFlow(context, billingFlowParams)
```

# コールバックを受信するリスナー用のインターフェース

`BillingClient`を使って課金機能を実装していくには、コールバックを受け取るためのインターフェースを実装したリスナー用のクラスを作成して渡していく必要があります。

ここでは、そのコールバック用のメソッドが定義された主なインターフェースについて簡単にまとめていきます。

## BillingClientStateListener

`BillingClient`の初期化時に呼び出される`startConnection()`にこのインターフェースを実装したクラスを渡す。

```kotlin
object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        // TODO: ...
    }

    override fun onBillingServiceDisconnected() {
        // TODO: ...
    }
}
```

### onBillingSetupFinished()

受け取った`BillingResult`の`responseCode`を確認します。
`BillingClient.BillingResponseCode.OK`を表している場合は、`BillingClient`の使用準備が整ったことを表します。

準備が整ったら、`ProductDetails`と`Purchases`の取得を行います。

```kotlin
override fun onBillingSetupFinished(billingResult: BillingResult) {
    val responseCode = billingResult.responseCode
    // val debugMessage = billingResult.debugMessage

    if (responseCode == BillingClient.BillingResponseCode.OK) {
        // ここにたどり着いた時点で、BillingClientの使用準備が完了したことを意味する。

        // ProductDetailsを取得する
        queryProductDetails()
        // Purchasesを取得する
        queryPurchases()
    }
}
```

### onBillingServiceDisconnected()

`BillingClient`の接続が切断された際に呼び出されます。通常は`startConnect()`を再度呼び出します。

```kotlin
override fun onBillingServiceDisconnected() {
    billingClient.startConnection(/* BillingClientStateListenerを実装したクラスを渡す */)
}
```

## PurchasesUpdatedListener

`BillingClient`の初期化時に呼び出す`setListener()`に、このインターフェースを実装したクラスを渡します。

### onPurchasesUpdated()

`onPurchasesUpdated()`では、購入ボタンをクリックして購入を完了させたり、戻るボタンをタップすることによって購入がキャンセルされたなどの、ユーザーが行った購入フローの最終結果を取得することができます。

第一引数で受け取った`billingResult`を通して、購入に成功したかどうかの判断を行うことができます。

第二引数の`purchaseList`では、このアプリを通してユーザーが行ったすべての購入が含まれた`Purchase`のリストを取得できます。
`Purchase`には、商品IDや、購入トークン、そして承認済みかどうかを表すフラグなどがプロパティとして格納されています。これらの値を用いて、承認作業が必要な新規の購入なのか、それともこれ以上処理を行う必要のない既存の購入を表しているかを判断することが可能となっています。

```kotlin
override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchases: List<Purchase>?
) {
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
       && !purchases.isNullOrEmpty()
   ) {
       // TODO: purchasesの要素をひとつひとつ調べて承認が必要なのかを判断して処理する
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
       // TODO: 購入がキャンセルされたときの処理
   } else {
       // TODO: 他のエラーの処理
   }
}
```

## PurchasesResponseListener

`BillingClient`の`queryPurchasesAsync()`の第二引数に、このインターフェースを実装したクラスを渡します。

```kotlin
billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
) { billingResult, purchaseList ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        // TODO: ...
    } else {
        // TODO: ...
    }
}
```

### onQueryPurchasesResponse()

既存の購入に関する情報を`billingResult`と`purchaseList`という引数を通じて取得します。
アプリの起動中に発生した新しい購入の情報については、上記で説明した`PurchasesUpdatedListener`で処理をします。
アプリが実行中でなかったときに`PURCHASED`ステータスに移行した購入情報を処理するために、`onResume()`のタイミングで、`queryPurchasesAsync()`を呼び出し、このメソッドで処理を完了することが望まれています。

```kotlin
override fun onQueryPurchasesResponse(
    billingResult: BillingResult,
    purchasesList: MutableList<Purchase>
) {
    // TODO: 未処理の購入（Purchase）を処理する
}
```

第二引数で受け取った`MutableList<Purchase>`が`null`かどうかを確認します。
加えて、アプリで保存してある以前の`MutableList<Purchase>`と変更がないかを確認します。

```kotlin
if (purchasesList == null || isUnchangedPurchaseList(purchasesList)) {
    Log.d(TAG, "processPurchases: Purchase list has not changed")
    return
}
```

## ProductDetailsResponseListener

`BillingClient`の`queryProductDetailsAsync()`の第二引数に、このインターフェースを実装したクラスを渡します。


```kotlin
object : ProductDetailsResponseListener {
    override fun onProductDetailsResponse(
        billingResult: BillingResult,
        productDetailsList: MutableList<ProductDetails>
    ) {
        // TOOD: ...
    }
}
```

### onProductDetailsResponse()

このメソッドでは、`queryProductDetailsAsync()`の呼び出し結果を`billingResult`と`productDetailsList`を通して受け取ります。

```kotlin
override fun onProductDetailsResponse(
    billingResult: BillingResult,
    productDetailsList: MutableList<ProductDetails>
) {
    val responseCode = billingResult.responseCode
    when (responseCode) {
        BillingClient.BillingResponseCode.OK -> {
            var newMap = emptyMap<String, ProductDetails>()
            if (!productDetailsList.isNullOrEmpty()) {
                newMap = productDetailsList.associateBy {
                    it.productId
                }
            }
            // TODO: ここで作成したnewMapをこのメソッド外の変数に保存できる仕組みを実装する
        }
        else -> {
            // TODO: エラーが発生した場合の処理を行う
        }
    }
}
```

`onProductDetailsResponse()`で受け取った`productDetailsList`の値を他の場所から参照できるにするために、商品IDがキーで、`ProductDetails`が値のマップを準備します。このメソッドの役割は、このマップに値を保存するところにあります。

もし溶融がある場合は、準備していた商品IDのリストと、格納した`ProductDetails`の個数が一致するかを確認します。一致しない場合は不具合が発生している可能性がありますので、対処を行います。

```kotlin
val productDetailsCount = productsWithProductDetails.size
if (productsWithProductDetails.size == LIST_OF_PRODUCTS.size) {
    // ...
} else {
    // ...
}
```

# 購入（Purhcase）を処理する際の共通の実装

`onQueryPurchasesResponse()`や`onPurchasesUpdated()`が行うべき処理のほとんどは共通しています。これらは、呼び出されるタイミングや文脈こそ違えど、受け取る引数の数や種類はほとんど一致しています。

それらのメソッドが呼ばれた際に行うべき`Purchase`に依存した共通の処理を確認していきましょう。
ここでは、定期購入の`Purchase`に対する処理を見ていきます。

まず最初に、受け取った`purchasesList`が`null`でない、かつ以前と変更点があるかを確認します。条件に合致した場合は定期購入の認証作業を行います。

`Purchase`が認証済みかどうかは、`isAcknowledged`プロパティを通じて確認できます。

取得した`purchasesList`の**acknowledge**と**not acknowledged**の件数を調べます。
購入したばかりの`Purchase`が最初に受信された時は、まだ承認されていないので、`isAcknowledged`は`false`になります。

```kotlin
for (purchase in purchasesList) {
    if (purchase.isAcknowledged) {
        // TODO: 承認済みだった場合の処理
    } else {
        // TODO: 未承認だった場合の処理
    }
}
```

Firebase Functionsなどに購入トークンを送信します。サーバー上で購入トークンとユーザーの紐付けを行います。
紐付けが完了した後は、サーバー側で認証作業を行うか、アプリ内で`BillingClient#acknowledgePurchase()`を呼び出して対象の`Purchase`の購入トークンを承認します。

`purchaseToken`を使って`AcknowledgePurchaseParams`インスタンスを作成します。

```kotlin
val params = AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchaseToken)
    .build()
```

`BillingClient#acknowledgePurchase()`に`AcknowledgePurchaseParams`インスタンスを渡して、購入トークンの承認を行います。


```kotlin
for (purchase in purchasesList) {
    if (purchase.isAcknowledged) {
        // TODO: 承認済みだった場合の処理
    } else {
        // NOTE: 未承認だった場合の処理
        val params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()

        billingClient.acknowledgePurchase(
            params
        ) { billingResult ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
                purchase.purchaseState == Purchase.PurchaseState.PURCHASED
            ) {
                // TODO: Purchaseが承認された場合に行う処理
            }
        }
    }
}
```

# PurchasesUpdatedListenerとPurchasesResponseListenerの両方を使用する理由

`PurchasesUpdatedListener`で`Purchase`の最新データをリッスンするだけでは、すべてのシナリオをカバーできないことが公式ドキュメントで記述されています。

詳しくはこちらをご覧ください。

https://developer.android.com/google/play/billing/integrate?hl=ja#fetch

`PurchasesUpdatedListener`が反応しない場合のシナリオは主に３つ存在します。

1. ユーザーは購入に成功しているものの、何らかの障害が発生したことによりスマートフォンのネットワーク接続が切れてしまい、Googleは購入した情報を送信しているものの`PurchasesUpdatedListener`が反応できないケース。
2. 同時に複数の端末で同じアプリを起動している状態で、一方の端末でのみ購入を行った場合には購入を行った端末には`PurchasesUpdatedListener`でカバーできるものの、もう一方の端末ではリッスンできないケース。
3. アプリ以外の場所で購入が行われたケース。


これらの問題に対処するために、必ずライフサイクルが`onResume()`の場合に`BillingClient`の`queryPurchasesAsync()`を呼び出して未処理の`Purchase`を処理することが求められています。

# まとめ

アプリ内課金と言っても、ご覧いただいた通り登場するクラスやメソッド、それにインターフェースがさまざまあり、承認自体もアプリ内で行うべきなのかサーバー側で行うべきなのか、サンプルリポジトリは用意してくれているものの、ドキュメントにわかりやすい実装方法が掲載されていないため、初見にとってはよくわからないという印象を強く抱かせてしまっています。

しかし、以下に掲載したDroidKaigiの発表など、日本語のかつわかりやすい解説などは探せばいくつか見つかります。（僕はランニングしながら、この動画の音声を聴いたことによりGoogle Play Billing Libraryの全体像を把握することができました。）

https://www.youtube.com/watch?v=UjZxICZXOa8

それらの情報を掻い摘みながら、自分のユースケースにとってベストな実装方法を選択していただく際の補助にこの記事がなってくれれば幸いです。

# 参考にした記事

https://github.com/android/play-billing-samples/tree/main/PlayBillingCodelab

https://zenn.dev/ataka/articles/08cef284cab7ee

https://www.youtube.com/watch?v=7cf8yHdXMdA

https://droidrocks.com/how-to-integrate-google-play-in-app-purchase-billing-library/

https://medium.com/@alimehdipk/implementing-google-play-billing-library-v4-for-in-app-and-subscriptions-f4bc74c58ed0

https://itneko.com/google-play-billing/

https://developer.android.com/google/play/billing/additional-resources?hl=ja

https://codelabs.developers.google.com/play-billing-codelab?hl=ja#3

https://www.jisei-firm.com/android_develop21/

https://developer.android.com/google/play/billing/migrate-gpblv5?hl=ja

https://developer.android.com/google/play/billing/integrate?hl=ja

https://github.com/android/play-billing-samples/tree/master/ClassyTaxiAppKotlin

https://github.com/android/play-billing-samples

https://qiita.com/takahirom/items/4d597b00f500efb3dc7f

https://qiita.com/orimomo/items/8439eb4a4bf171adbeb1

https://itneko.com/google-play-billing/

https://qiita.com/keyoris/items/552e83347fafa99a2998

https://www.geeksforgeeks.org/play-audio-from-url-in-android-using-kotlin/

https://developer.android.com/google/play/billing/integrate?hl=ja