LoginSignup
2
0

More than 3 years have passed since last update.

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

Posted at

背景

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ストアの課金画面が以下のようになり、実際には課金が行われない、フェイクの画面であることが分かるようになります。

テスト購入が可能なテストユーザー登録という方法もあるのですが、アカウント管理が別部署だったりして手続きが煩雑な場合など、まずは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を載せておきます。

プロジェクトのbuild.gradle
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
}

アプリのbuild.gradle
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"
}

可否

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

方法

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

1.SkuDetailsを先に得る

    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を使用して課金フローを起動する

    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と書いているのは、サーバー側の処理を呼び出す想定で作ったサンプルだからですが、実際には何も呼んでいません。通常はサーバー側で署名やレシートの検証、ポイント等を付与を行うと思うのでこのタイミングでよしなにやってください。

参考サイト

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も私が以前触ったときとはまたバージョンが変わっているようで、StateFlowとか勉強になりました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0