前提
現在、オリジナルのカメラアプリを作成しています。
課金をすることで、録画機能が拡張される仕様のものを作成しています。
課金のテストはシミュレーターでは行えないため、実機を用いて行っています。
また、テスト用のアカウントをplay consoleに登録して、実際には料金の支払いを行わなくても、課金をした時と同じような挙動を確認できるようにしています。
やりたいこと
- 課金前に録画ボタンを押すと、アイテム購入を処理する画面が表示される。
(購入するアイテムはアプリの機能を拡張する「非消費型」のものとする。)
(機能を拡張すると、カメラの録画機能が使用できるようになる。)
2. Google Playのサーバーと通信して、課金処理を完了させる。
3. 課金後に再度録画ボタンを押すと、購入を処理する画面は表示されず、録画が開始されるようにしたい。
できていること
・ボタンを押して録画の実行と停止を行う機能の実装
・「やりたいこと」の1.と 2.の機能の実装
できていないこと
「やりたいこと」の3.の機能の実装。
現状、課金後に録画ボタンを押すと、録画は実行されず「このアイテムはすでに所有しています。」というメッセージと共に、ウィンドウが表示されてしまいます。
コード
現在作成中のコードの該当箇所を以下に記載します。
purchaseSWという変数に購入状況を渡して、処理を分岐させようとしています。
各所で購入できるアイテムがない場合(既に購入が完了した場合)は、値をtrueに変更しています。
kotlin
1// 録画ボタンを押した際に呼ばれる 2 @RequiresApi(Build.VERSION_CODES.O) 3 override fun onClick(view: View) { 4 Log.i("録画", "録画ボタンアクション:${isRecordingVideo}") 5 if(!purchaseSW) { // 購入が完了していない場合 6 setUpBillingClient() // 課金の購入フローのセットアップ 7 } else { 8 when (view.id) { // ビューが生成されている場合 9 R.id.button_rec_anim -> if (isRecordingVideo) { 10 // 録画停止処理(割愛) 11 } else { 12 // 録画開始処理(割愛) 13 } 14 } 15 } 16 } 17 18... 19 20// 購入の最新情報を取得するイベントリスナー(googlePlayがonPurchasesUpdated()を実行して通知を受ける) 21 private val purchasesUpdatedListener = 22 PurchasesUpdatedListener { billingResult, purchases -> 23 24 } 25 26 // BillingClientの作成,初期化(Google Play Billing Libraryとアプリの通信に使用する) 27 private fun setUpBillingClient() { 28 billingClient = BillingClient 29 .newBuilder(requireActivity()) 30 .setListener(purchasesUpdatedListener) 31 .enablePendingPurchases() 32 .build() 33 startConnection() // 接続開始 34 } 35 36 // Billing Serviceへの接続を開始する 37 private fun startConnection() { 38 billingClient?.startConnection(object : BillingClientStateListener { 39 // Billing Serviceへ接続した際の処理 40 override fun onBillingSetupFinished(billingResult: BillingResult) { 41 // サーバーへの接続が正常に行われた(OK)場合 42 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { 43 Log.v("TAG_INAPP","Setup Billing Done") 44 // 購入可能なアイテムを取得する 45 queryAvaliableProducts() 46 } 47 } 48 // Billing Serviceから切断された際の処理 49 override fun onBillingServiceDisconnected() { 50 Log.v("TAG_INAPP","Billing client Disconnected") 51 } 52 }) 53 } 54 55 // 購入可能なアイテムを取得する 56 private fun queryAvaliableProducts() { 57 val skuList = ArrayList<String>() 58 skuList.add("商品のID(割愛)") // Google Play Console で登録した商品IDの文字列を指定 59 val params = SkuDetailsParams.newBuilder() 60 params.setSkusList(skuList).setType(INAPP) // 商品情報を設定(skuの名前とタイプ(INAPP:1回限り、SUBS:定期購入)) 61 62 // 設定した商品の詳細を取得する 63 billingClient?.querySkuDetailsAsync(params.build()) { billingResult, skuDetailsList -> 64 // サーバーとの接続ができていて、且つ購入可能な商品がある場合は表示する 65 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && !skuDetailsList.isNullOrEmpty()) { 66 for (skuDetails in skuDetailsList) { 67 // SKUの情報を表示する 68 Log.v("TAG_INAPP","skuDetailsList : ${skuDetailsList}") 69 // querySkuDetailsAsync()を呼び出してskuDetailsの値を取得する 70 val flowParams = BillingFlowParams.newBuilder() 71 .setSkuDetails(skuDetails) 72 .build() 73 // 購入ダイアログを表示する 74 billingClient.launchBillingFlow(requireActivity(), flowParams).responseCode 75 } 76 purchaseSW = false 77 } else { 78 purchaseSW = true 79 } 80 } 81 } 82 83 // PurchasesUpdatedListenerに購入の状態を通知する(購入成功後googlePlayにより呼び出される) 84 suspend fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) { 85 // 購入を実行していて、且つ購入できるアイテムリストが存在する場合 86 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { 87 for (purchase in purchases) { // 購入できるアイテムをリストから順に取得 88 handlePurchase(purchase) // 各アイテムの購入状態を確認する 89 } 90 } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { 91 // ユーザーが購入フローをキャンセルした場合 92 } else { 93 // 購入アイテムが存在しない場合 94 purchaseSW = true 95 } 96 } 97 98 // 購入の状態を確認する 99 suspend fun handlePurchase(purchase: Purchase) { 100 // 購入が完了している場合 101 if (purchase.purchaseState === Purchase.PurchaseState.PURCHASED) { 102 // まだ購入が承認されていない場合 103 if (!purchase.isAcknowledged) { 104 val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() 105 .setPurchaseToken(purchase.purchaseToken) // アイテムと購入トークン(アイテムの利用権)を紐付ける 106 val ackPurchaseResult = withContext(Dispatchers.IO) { 107 // 消費不可アイテムの購入を承認する 108 billingClient.acknowledgePurchase(acknowledgePurchaseParams.build()) 109 } 110 } else { 111 purchaseSW = true 112 } 113 } 114 }
エラー
目立ったエラーは出ていないのですが、変数「purchaseSW」の値が反映されていないようで、条件分岐がうまくできていません。
お手数ですが、原因がお分かりになる方がいらっしゃいましたらご教授いただけますと幸いです。
参考にしたサイト
公式リファレンス - Google Play の課金システムの概要
公式リファレンス - アプリの公開手順
enoiu - aabファイルの作り方
公式リファレンス - Google Play Consoleの準備
公式リファレンス - 課金ライブラリをアプリに統合する方法
Code Camp - テスト用課金アプリを簡単に作ってみる
ICHI.PRO - Androidアプリにアプリ内購入を実装する方法
公式リファレンス - アプリ ライセンスを使用したアプリ内課金のテスト
Qiita - AndroidでGoogle Play月額課金を実装する
公式リファレンス - queryPurchasesAsyncについて
Medium - Google Playアプリ内課金の実装方法ガイド
【追記 1(2021.11.5)】
その後色々と試していると、「google playとの通信中には変数の値の書き換えができない」ことがわかりました。
そのため、google playとの通信のキャッシュを活用して、onResumeのタイミングで購入状況を確認する方法に変更しようと考えています。
アイテムが購入されているかを確認するためには、「queryPurchasesAsync()」を使用すると下記の公式リファレンスに説明があったので、リファレンスを参考に実装を進めています。
公式リファレンス - queryPurchasesAsync()を使ったアプリ外購入の確認
公式リファレンス - queryPurchasesAsync()
追加の質問事項
「queryPurchasesAsync()」はsuspend関数内で実行する必要があります。
そのため、onResume内で普通に「queryPurchasesAsync()」と記述して実行することができません。
kotlin
1// Activity(画面)が表示、再開された際に呼ばれる 2 @RequiresApi(Build.VERSION_CODES.M) 3 override fun onResume() { 4 super.onResume() 5 // ビューの設定ができている場合(再開時に実行する) 6 if (textureView.isAvailable) { 7 // 購入履歴をチェック 8 queryPurchases() // ← onResumeもsuspendにしてください。とエラーが出る。 9 ... 10 } else { // ビューの設定がまだ完了していない場合 11 ... 12 } 13 } 14 15... 16 17// google playのキャッシュに購入履歴がないか確認する 18 suspend fun queryPurchases() { 19 val result = billingClient.queryPurchasesAsync(skuType = BillingClient.SkuType.INAPP) 20 if(result.purchasesList != null) { // 何かしらの購入履歴がキャッシュに残っている場合 21 purchaseSW = true 22 } 23 }
抜粋ですがコードは上記のようになっています。
<質問>
「onResume内で、suspendの関数を実行するためには、どのように記述すればよいのでしょうか。」
【追記 2(2021.11.5)】
上記の追加質問について、下記のサイトを参考にonResume内のsuspend関数を「runBlocking{}」で囲むことで一旦はエラーなく実装することができました。
kotlin
1// Activity(画面)が表示、再開された際に呼ばれる 2 @RequiresApi(Build.VERSION_CODES.M) 3 override fun onResume() { 4 super.onResume() 5 // ビューの設定ができている場合(再開時に実行する) 6 if (textureView.isAvailable) { 7 // 購入履歴をチェック 8 if (billingClient.isReady) { 9 runBlocking { 10 queryPurchases() 11 } 12 } 13 ... 14 } else { // ビューの設定がまだ完了していない場合 15 ... 16 } 17 } 18 19... 20 21// google playのキャッシュに購入履歴がないか確認する 22 suspend fun queryPurchases() { 23 val result = billingClient.queryPurchasesAsync(skuType = BillingClient.SkuType.INAPP) 24 if(!result.purchasesList.isNullOrEmpty()) { // 何かしらの購入履歴がキャッシュに残っている場合 25 purchaseSW = true 26 } 27 }
また、キャッシュに購入履歴がないかを確認する方法として、下記のような条件を設定しています。
if(!result.purchasesList.isNullOrEmpty())
現状
録画ボタンを押すと、1回目は購入ダイアログが表示されます。
ですが、購入したかどうかに関わらず、2回目以降は購入ダイアログが表示されず、録画が実行されます。
<推測>
購入のキャッシュが端末から完全に削除されていないことが原因で、onResume内でqueryPurchasesが実行された瞬間、purchaseSWをtrueにしているのかもしれません。
そのため、購入したかどうかに関わらず、2回目以降は購入ダイアログが表示されていない可能性があります。
追加の質問事項
・suspendの関数を実行するにあたり、現在の実装方法は正しいのでしょうか。
・今回のコードでファイルが空かどうか確認するために「isNullOrEmpty()」を使用するのは正しいのでしょうか。
・アプリを起動したタイミングで購入のキャッシュを確認するためにはどのような実装方法があるのでしょうか。
あなたの回答
tips
プレビュー