Play Billing Library 3.0でstatic responseな購入のテストをする

# 背景
Androidのアプリ内課金は2021年11月1日以降、BillingLibrary3.0以上を搭載していないとアプリのアップデートがリジェクトされるようなる。
https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.html

その課金ライブラリの移行にあたり、AIDLまでの時代に出来ていた、Static Responseと呼ばれるもので実際には課金が行われないテストが出来るか確認したかった。

## Static Responseとは

アプリ内課金では、sku(課金アイテムID)を指定して、登録済の課金アイテムを購入しますが、以下のようなskuを指定することで固定のレスポンスを得ることが出来るというものです。

- `android.test.purchased`
- `android.test.canceled`
- `android.test.refunded`
- `android.test.item_unavailable`

参考)https://stuff.mit.edu/afs/sipb/project/android/docs/google/play/billing/billing_testing.html

これらを指定すると、Playストアの課金画面が以下のようになり、実際には課金が行われない、フェイクの画面であることが分かるようになります。

|<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/ce1f89b2-0af3-6caa-c96b-45c1367ef321.png">|
|:--|

テスト購入が可能なテストユーザー登録という方法もあるのですが、アカウント管理が別部署だったりして手続きが煩雑な場合など、まずはStatic Responseでフローを確認したいという状況は多々あると思いますので、そんな場合に参考になれば幸いです。


# 環境
|ツール・ライブラリなど|バージョン|
|:--|:--|
|Android Studio| 3.6.3|
|Kotlin|1.3.71|
|Gradle|5.6.4|
|Android Gradle Plugin|3.6.3|
|Billing Library|3.0.0|

参考までに、プロジェクトとアプリのの`build.gradle`を載せておきます。

<details><summary>プロジェクトの`build.gradle`</summary><div>

```project/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.71'
    ext.lifecycleVersion = "2.2.0"
    ext.gradle_version = '3.6.3'
    ext.roomVersion = "2.2.5"
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

</div></details>


<details><summary>アプリの`build.gradle`</summary><div>

```app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    dataBinding {
        enabled true
    }

    defaultConfig {
        applicationId "com.example.billingsample"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    apply from: '../key.gradle', to: android

    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    flavorDimensions 'lib'
    productFlavors {
        aidl {
            dimension = 'lib'
            manifestPlaceholders = [appName: 'AIDLサンプル']
        }
        pbl {
            dimension = 'lib'
            manifestPlaceholders = [appName: 'PBLサンプル']
        }
    }
    sourceSets {
        aidl {
            java {
                srcDirs 'src/aidl/java', 'src/aidl/java/'
            }
        }
        pbl {
            java {
                srcDirs 'src/pbl/java', 'src/pbl/java/'
            }
        }
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // appended
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
    implementation 'com.google.android.material:material:1.2.1'

    // ViewModel and liveData
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
    implementation 'androidx.fragment:fragment-ktx:1.2.5'
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"

    // Databinding
    kapt "androidx.databinding:databinding-common:$gradle_version"

    // Room
    implementation "androidx.room:room-runtime:$roomVersion"
    kapt "androidx.room:room-compiler:$roomVersion"
    implementation "androidx.room:room-ktx:$roomVersion"

    // coroutine
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'

    // Only for PBL flavor
    def billing_version = "3.0.0"

    pblImplementation "com.android.billingclient:billing:$billing_version"
    pblImplementation "com.android.billingclient:billing-ktx:$billing_version"
}

```

</div></details>

# 可否
まず出来るのかできないかですが、結論から言うと、**可能**です。

# 方法
PBL版では、購入時に`SkuDetails`という情報が必要になります。
これは、`BillingClient#querySkuDetailsAsync`という関数で事前に引っ張っておくのが通常かと思います。
Static Responseも、同じようにこの関数を挟んで`SkuDetails`を取れば、`BillingClient#launchBillingFlow`に渡すことが出来て購入のテストが可能になります。

## 1.SkuDetailsを先に得る

```kotlin
    private val skuDetailsMap = mutableMapOf<String, SkuDetails>()
    private val skuList = listOf("android.test.purchased")

    private fun querySkuDetails(){
        val params = SkuDetailsParams.newBuilder().setSkusList(skuList).setType(BillingClient.SkuType.INAPP).build()
        billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
            when (billingResult.responseCode) {
                BillingClient.BillingResponseCode.OK -> {
                    if (skuDetailsList.orEmpty().isNotEmpty()) {
                        skuDetailsList?.forEach {
                            skuDetailsMap[it.sku] = it
                        }
                    }
                }
                else -> {
                    Log.e(TAG, billingResult.debugMessage)
                }
            }
        }
    }
```

## 2.SkuDetailsを使用して課金フローを起動する

```kotlin
    fun purchaseStaticItem(activity: Activity) {
        // 購入用のパラメータを作成
        val skuDetails = skuDetailsMap["android.test.purchased"]
        skuDetails?.let {
          val purchaseParams = BillingFlowParams.newBuilder()
              .setSkuDetails(it)
              .build()
          billingClient.launchBillingFlow(activity, purchaseParams)
        }
    }
```

これだけです。

# 参考プロジェクト
AIDL版とPBL版をProduct Flavorで切り替えられるサンプルプロジェクトを作りました。
移行の参考などにして貰えたらと思います。

注意点としては、AIDL版のStatic Responseによる購入は、なぜか消費しなくても何度も買えてしまいます。以前は毎回消費が必要だったように記憶しているのですが、AIDLの内部バージョンで違うのでしょうかね。

また、買い切りタイプやサブスク(定期購読)には対応しておらず、**消費可能アイテム**にのみ対応しています。
コミットAPIと書いているのは、サーバー側の処理を呼び出す想定で作ったサンプルだからですが、実際には何も呼んでいません。通常はサーバー側で署名やレシートの検証、ポイント等を付与を行うと思うのでこのタイミングでよしなにやってください。

https://github.com/les-kasa-1010/BillingLibrarySample3


# 参考サイト

## Static Responseの件
https://www.366service.com/jp/qa/417a0cdef1cc694aaa23928a67faf9d6
中国語の質問板を機械翻訳したもののようですが、非常に参考になりました。


## coroutine関係
https://star-zero.medium.com/callback%E5%BD%A2%E5%BC%8F%E3%81%AE%E3%82%82%E3%81%AE%E3%82%92coroutines%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B-9384dfa6ad77
コールバック形式のものの処理結果をViewModelで待って受け取る方法の参考にしました。

https://stackoverflow.com/questions/61388646/billingclient-billingclientstatelistener-onbillingsetupfinished-is-called-multip
PurchasesUpdatedListener#onPurchasesUpdatedの購入結果をViewModelで非同期処理を待って受け取る方法の参考にしました。
質問者さんのコードが参考になりました。(質問者さんの質問本題はよく読んでいませんw)


# 余談
Android Extensionsがdeprecatedになるようですが、そのまま使っています。
coroutineも私が[以前触ったとき](https://qiita.com/kasa_le/items/7d680cb0c03d28c817a7)とはまたバージョンが変わっているようで、`StateFlow`とか勉強になりました。