Help us understand the problem. What is going on with this article?

Google Play Billing Library を使ってみる

More than 1 year has passed since last update.

はじめに

Google Play Billing Library を使って、アプリ内課金を実装してみます。
このコードラボを参考にしてます。
https://github.com/googlecodelabs/play-billing-codelab

課金の種類について

日常的に触れる課金は、以下のように分類されると思います。

  • 消費型商品
    使う度に消費されるアプリ内通貨や、回復アイテムなど

  • 非消費型商品
    広告削除、機能のアンロックなど、一度購入したら永続的に使えるもの

  • 定期購入型商品
    開発者が指定した期間毎に自動的に購入されるもの
    Netflixなどの月額課金などと呼ばれるもの

ちなみに 消費型非消費型 と分けましたが、これらは厳密に分類されてるわけではなく、
購入した商品を消費するか、永続的に持つかどうかの実装の違いしかありません。

実装

ライブラリの導入

build.gradleのdependenciesに1行追加しましょう

dependencies {
    ...
    implementation 'com.android.billingclient:billing:1.2.1'
}

BillingClientのセットアップ

fun setup() {
  // ユーザーが商品を購入した時に呼ばれる
  val purchaseUpdateListener = object : PurchasesUpdatedListener {
    override fun onPurchasesUpdated(
        responseCode: Int,
        purchases: MutableList<Purchase>?
    ) {
      Log.d("onPurchasesUpdated", "responseCode:$responseCode")
      Log.d("onPurchasesUpdated", "purchase:${purchases?.toString()}")
      purchases?.forEach { purchase ->
        Log.d("onPurchasesUpdated", "purchase:$purchase")
      }
    }
  }

  // BillingClient : メインに使われるインターフェース
  val billingClient: BillingClient = BillingClient
      .newBuilder(Activity())
      .setListener(purchaseUpdateListener)
      .build()

  // セットアップ処理のコールバック
  val clientStateListener = object : BillingClientStateListener {
    override fun onBillingServiceDisconnected() {
      Log.d("onBillingServiceDisconnected", "Service disconnected.")
    }

    override fun onBillingSetupFinished(responseCode: Int) {
      if (responseCode == BillingClient.BillingResponse.OK) {
        Log.d("onBillingSetupFinished", "Setup success. responseCode:$responseCode")
      } else {
        Log.w("onBillingSetupFinished", "Setup error. responseCode:$responseCode")
      }
    }
  }

  // 接続開始
  billingClient.startConnection(clientStateListener)
}

商品の詳細情報を取得

商品を購入するために、商品の詳細情報 SkuDetails が必要になります。

querySkuDetailsAsync

skuTypesku を指定して詳細情報を非同期で取得出来ます。

// BillingClient.SkuType.INAPP : アプリ内通貨や、消費アイテムなど
val skuType = BillingClient.SkuType.INAPP
// SKU : 固有の商品 ID
val inAppSkus = listOf("gas", "premium")
val params = SkuDetailsParams.newBuilder()
      .setType(skuType)
      .setSkusList(inAppSkus)
      .build()

// 取得開始
billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
  if (responseCode == BillingClient.BillingResponse.OK) {
    Log.d(TAG, skuDetailsList.toString())
  }
}

こんな感じのデータが返ってきます

D/querySkuDetailsAsync:
[SkuDetails: {
  "productId": "gas",
  "type": "inapp",
  "price": "¥111",
  "price_amount_micros": 110686158,
  "price_currency_code": "JPY",
  "title": "Gas (Play Billing Codelab)",
  "description": "Buy gasoline to ride!"
},
SkuDetails: {
  "productId": "premium",
  "type": "inapp",
  "price": "¥166",
  "price_amount_micros": 166029237,
  "price_currency_code": "JPY",
  "title": "Upgrade your car (Play Billing Codelab)",
  "description": "Buy a premium outfit for your car!"
}]

以下、この SkuDetails を使って進めていきます。

商品を購入

// 購入フローを開始するためのパラメータ
val billingFlowParams = BillingFlowParams.newBuilder()
      .setSkuDetails(skuDetails)
      .build()

// 購入フローを開始します
billingClient.launchBillingFlow(activity, billingFlowParams)

以下のように購入ダイアログが表示されます。

購入後に PurchasesUpdatedListener#onPurchasesUpdated() が呼ばれます。

D/onPurchasesUpdated: responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json: // 購入した商品の情報が返ってくる
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium", // SKU
  "purchaseTime": ...,
  "purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
  "purchaseToken" ...:
}

追加でもう一度購入したいので、再び購入処理を行ってみると

W/BillingClient: Unable to buy item, Error response code: 7

エラーログが出てる…エラーコードを確認しましょう。

BillingClient.BillingResponse
/** Failure to purchase since item is already owned */
int ITEM_ALREADY_OWNED = 7;

既に所有しているとのこと。

商品を消費

商品を再び買うためには、購入済みの商品を 消費 しなければいけません。
BillingClient#consumeAsync (String purchaseToken, ConsumeResponseListener listener) を呼んで消費しましょう。
(※SkuType.SUBS の定期購入型商品は消費出来ません。)

purchaseToken の取得

購入後に呼ばれる onPurchasesUpdatedpurchaseToken 含む情報が返ってきてましたが、保存していなくても後から BillingClient#queryPurchases() で問い合わせる事が出来ます。

fun queryInAppPurchases() {
  // アプリ内で購入した指定 SkuType の商品の購入詳細を取得します
  val purchaseResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)

  // ログ確認
  Log.d("queryPurchases", "responseCode:${purchaseResult.responseCode}")
  Log.d("queryPurchases", purchaseResult.purchasesList.toString())
}

以下のようなデータが返ってきます。

D/queryPurchases: responseCode:0 // BillingClient.BillingResponse.OK
D/queryPurchases: Purchase. Json:
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium", // SKU
  "purchaseTime": ...,
  "purchaseState": ..., // 注文の購入状況 (0:Purchased, 1:Canceled)
  "purchaseToken" ...:
}

もしアプリをアンインストールしてしまって、購入情報を失ってしまった場合も、購入時のGoogleアカウントが紐付いていれば、この情報を使って購入済みの状態を復元する事が出来そうです。

purchaseToken を使って商品を消費する

fun consume(purchaseToken: String) {
  // 消費操作が終了した時に呼ばれるリスナー
  val consumeResponseListener = object : ConsumeResponseListener{
    override fun onConsumeResponse(responseCode: Int, purchaseToken: String?) {
      Log.d("onConsumeResponse", "responseCode:$responseCode")
      Log.d("onConsumeResponse", "purchaseToken:$purchaseToken")
    }
  }

  // purchaseToken とリスナーを渡して消費処理開始
  billingClient.consumeAsync(purchaseToken, consumeResponseListener)
}

消費ログを確認

D/onConsumeResponse: responseCode:0 // BillingClient.BillingResponse.OK
D/onConsumeResponse: purchaseToken:......

消費に成功してそうなので、もう一度 BillingClient#queryPurchases() で購入情報を確認してみましょう。

D/queryPurchases: responseCode:0
D/queryPurchases: []

リストが空になってますね。
購入してる商品が無い状態なので、購入処理を再度呼んでみます。

D/onPurchasesUpdated:responseCode:0 // BillingClient.BillingResponse.OK
D/onPurchasesUpdated: purchase:[Purchase. Json:
{
  "orderId": ...,
  "packageName": com.codelab.sample,
  "productId": "premium",
  "purchaseTime": ...,
  "purchaseState": ...,
  "purchaseToken" ...:
}

購入成功しました。
アプリ内課金の基本的な流れは以上となります。

BillingClientをもう少し使ってみる

アプリ内課金のための便利な方法を提供してくれます。
他のメソッドも使ってみましょう。

queryPurchaseHistoryAsync

購入が期限切れ、キャンセル、または消費された場合でも、ユーザーによって行われた最新の購入を返します。

fun queryPurchaseHistoryAsync() {
  // リスナーを用意
  val purchaseHistoryResponseListener = object :PurchaseHistoryResponseListener{
    override fun onPurchaseHistoryResponse(responseCode: Int,
        purchasesList: MutableList<Purchase>?) {
      Log.d(TAG, "responseCode:$responseCode")
      Log.d(TAG, purchasesList?.toString())
    }
  }

   // SkuTypeとリスナーを渡して呼び出し
   billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, purchaseHistoryResponseListener)
}

レスポンス

D/BillingManager: responseCode:0 // BillingClient.BillingResponse.OK
D/BillingManager: [Purchase.Json: {
    "productId": "gas",
    "purchaseToken": ......
    "purchaseTime": ......
    "developerPayload": ......
}, Purchase.Json: {
    "productId": "premium",
    "purchaseToken": ......
    "purchaseTime": ......
    "developerPayload": ......
}]

同じ商品を複数回買っていた場合、最新の購入情報が返ってきます。

launchPriceChangeConfirmationFlow

ユーザが購読している商品の価格が変わったとき、このフローを起動してユーザに価格変更情報のある画面を表示させます。ユーザーは新しい価格を確認するか、またはフローをキャンセルすることができます。

※↓の処理の前に sku == "gold_monthly" の定期購読商品を定期購入しています。

fun launchPriceChangeConfirmationFlow() {
  // PriceChangeFlowParams の生成に SkuDetails が必要なので、取得します
  val skuType = BillingClient.SkuType.SUBS
  val subsSkus = listOf("gold_monthly", "gold_yearly")
  val skuDetailsParams = SkuDetailsParams.newBuilder()
      .setType(skuType)
      .setSkusList(subsSkus)
      .build()

  // SkuDetailsを取得する
  billingClient.querySkuDetailsAsync(skuDetailsParams) { responseCode, skuDetailsList ->
    // SkuType.SUBS の SkuDetailsList から SKU が gold_monthly の SkuDetails を取る
    val gold = skuDetailsList.filter { it.sku == "gold_monthly" }[0]
    // パラメータを生成する
    val priceChangeFlowParams = PriceChangeFlowParams.newBuilder()
        .setSkuDetails(gold)
        .build()

    // 価格変更確認処理を開始する
    billingClient.launchPriceChangeConfirmationFlow(activity, priceChangeFlowParams) {responseCode ->
      Log.d(TAG, "responseCode:$responseCode")
    }
  }
}

実際に価格変更は試していませんが、
価格の変更をユーザーに知らせて、契約更新を選べるんでしょうか。

関連リンク

https://developer.android.com/google/play/billing/billing_library_overview
https://developer.android.com/google/play/billing/billing_onetime
https://github.com/googlecodelabs/play-billing-codelab

以上です。

nyandroid
猫奴隷 / Androider
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした