背景
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`
// 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`
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
とか勉強になりました。