前言
最近看群聊宰闰,一位兄弟去面試安卓SDK崗位辛块,面試時候被問到了google play結(jié)算流程浊猾。這位兄弟平時主要是負(fù)責(zé)國內(nèi)的SDK渠道,海外SDK基本是沒有了解阶冈。
結(jié)果面試過程一臉尷尬闷尿,面完后在群里也分享了一下面試過程,正好最近公司要更新一下google play 結(jié)算庫 4.0女坑,順便我做個分享填具,希望群里哪位兄弟能看見 。
了解一下最近幾個版本結(jié)算庫的變化
Google Play 結(jié)算庫 4.0 版 (2021-05-18)
當(dāng)前最新版本,變更內(nèi)容
添加了 BillingClient.queryPurchasesAsync() 以替換 BillingClient.queryPurchases()匆骗,我們將在未來的版本中移除后者劳景。
添加了新的訂閱替換模式 IMMEDIATE_AND_CHARGE_FULL_PRICE。
添加了 BillingClient.getConnectionState() 方法碉就,用于檢索 Play 結(jié)算庫的連接狀態(tài)盟广。
更新了 Javadoc 和實現(xiàn),用于指明可在哪個線程上調(diào)用方法以及發(fā)布哪些線程結(jié)果瓮钥。
-
添加了 BillingFlowParams.Builder.setSubscriptionUpdateParams() 作為發(fā)起訂閱更新的新方式筋量,用于替換已移除的
BillingFlowParams#getReplaceSkusProrationMode、 BillingFlowParams#getOldSkuPurchaseToken碉熄、BillingFlowParams#getOldSku毛甲、BillingFlowParams.Builder#setReplaceSkusProrationMode 和BillingFlowParams.Builder#setOldSku。
添加了 Purchase.getQuantity() 和 PurchaseHistoryRecord.getQuantity()具被。
添加了 Purchase#getSkus() 和 PurchaseHistoryRecord#getSkus(),用于替換已移除的 Purchase#getSku 和 PurchaseHistoryRecord#getSku只损。
移除了 BillingFlowParams#getSku一姿、BillingFlowParams#getSkuDetails 和 BillingFlowParams#getSkuType。
Google Play 結(jié)算庫 3.0.3 版 (2021-03-12)
修復(fù)了在調(diào)用 endConnection() 時發(fā)生內(nèi)存泄漏的問題跃惫。
修復(fù)了利用單個任務(wù)啟動模式的應(yīng)用在使用 Google Play 結(jié)算庫時出現(xiàn)的問題叮叹。當(dāng)應(yīng)用從 Android 啟動器恢復(fù)運行且結(jié)算對話框在暫停之前可見時,將觸發(fā) onPurchasesUpdated() 回調(diào)爆存。
Unity 問題修復(fù)
更新到了 Java 3.0.3蛉顽,解決了內(nèi)存泄漏問題,并解決了當(dāng)應(yīng)用從 Android 啟動器恢復(fù)運行且結(jié)算對話框在暫停之前可見時出現(xiàn)的無法購買的問題先较。
Google Play 結(jié)算庫 3.0.2 版 (2020-11-24)
問題修復(fù)
修復(fù)了 Kotlin 擴展程序中的一個錯誤:協(xié)程失敗并顯示錯誤“Already resumed”携冤。
修復(fù)了將 Kotlin 擴展程序與 kotlinx.coroutines 庫版本 1.4 及更高版本一起使用時未解析的引用。
Google Play 結(jié)算庫 3.0.1 版 (2020-09-30)
問題修復(fù)
- 修復(fù)了以下錯誤:如果在結(jié)算過程中終止后恢復(fù)應(yīng)用闲勺,系統(tǒng)可能不會使用購買結(jié)果調(diào)用 PurchasesUpdatedListener曾棕。
Google Play 結(jié)算庫 4.0版需要關(guān)心的事情
根據(jù)需求,我司游戲采用了一次性商品模式菜循,訂閱模式相關(guān)不作說明翘地,本文著重講解一次性商品模式晴及,往下看我會解釋訂閱和一次性商品兩種模式的概念
一次性模式建議用BillingClient.queryPurchasesAsync(),BillingClient.queryPurchases()后續(xù)會被刪除掉臼婆。此方法官方描述:異步操作,返回活動訂閱和非消費的一次性購買。
這個方法使用谷歌Play Store應(yīng)用的緩存及皂,而不需要發(fā)起網(wǎng)絡(luò)請求。注:建議購服務(wù)端通過調(diào)用下面文檔接口做安全驗證
BillingClient.getConnectionState()方法挑庶,用于檢索 Play 結(jié)算庫的連接狀態(tài)亚侠。這個方法我查一下官方Demo并沒有使用,Google搜索也并沒有使用對應(yīng)blog渴杆,暫不建議使用寥枝。
新增兩個方法
Purchase.getQuantity() :表示應(yīng)用內(nèi)付費購買。返回購買的商品數(shù)量 磁奖,訂閱方式 固定返回1囊拜、一次性商品方式大于1
PurchaseHistoryRecord.getQuantity():應(yīng)用內(nèi)付費購買歷史記錄,返回購買的商品數(shù)量 比搭,訂閱方式 固定返回1 冠跷、一次性商品方式大于1
Purchase.getSku()被Purchase.getSkus() 代替了,getSku()方法被刪除掉 身诺。
還有一個重點 endConnection() 升級到4.x修復(fù)了內(nèi)存泄漏的問題蜜托。
BillingClient重要說明
BillingClient用于庫和用戶應(yīng)用程序代碼之間通信的主界面。為應(yīng)用內(nèi)計費提供了便利的方法霉赡。應(yīng)用程序創(chuàng)建該類的一個實例橄务,并使用它來處理應(yīng)用程序內(nèi)的計費操作。它為許多常見的應(yīng)用內(nèi)計費操作提供了同步(阻塞)和異步(非阻塞)方法穴亏。
強烈建議一次只實例化一個BillingClient實例蜂挪,以避免多個PurchasesUpdatedListener。onPurchasesUpdated(BillingResult, List) 回調(diào)單個事件嗓化。
所有帶AnyThread注釋的方法都可以從任何線程調(diào)用棠涮,所有異步回調(diào)都將在同一個線程上返回。用UiThread注釋的方法應(yīng)該從Ui(主)線程調(diào)用刺覆,所有的異步回調(diào)也將在Ui(主)線程上返回严肪。
實例化后,必須執(zhí)行設(shè)置才能開始使用該對象谦屑。要執(zhí)行設(shè)置驳糯,調(diào)用startConnection(BillingClientStateListener)方法并提供一個監(jiān)聽器;當(dāng)完成時,該監(jiān)聽器將得到通知氢橙,在此之后(而不是之前)结窘,你可以開始調(diào)用其他方法。
當(dāng)你使用完這個對象時充蓝,不要忘記調(diào)用endConnection()以確保正確的清理該對象隧枫。該對象持有與應(yīng)用內(nèi)計費服務(wù)和管理器的綁定喉磁,用于處理廣播事件,除非你能正確地處理它官脓,否則廣播事件將泄漏协怒。
如果你在Activity.onCreate(Bundle)方法中創(chuàng)建了對象,那么建議將endConnection()方法放到Activity.onDestroy()方法中卑笨。清理后孕暇,不能再為連接重用它。
如何處理BillingClient.onBillingServiceDisconnected()
關(guān)于是否在Google服務(wù)斷開后重連的問題赤兴,建議通過回調(diào)讓游戲做提示妖滔,不做重連邏輯。在4.0官方demo中注釋寫到嘗試再次連接服務(wù) 桶良,其實是什么都沒有做的座舍。
在查閱一番資料在v3.0結(jié)算庫斷開后重新連接,可能會出現(xiàn)異常閃退的問題陨帆。4.0中每次購買前可以做以下邏輯判斷曲秉。
BillingClient是否初始化成功
BillingClient 是否 isReady()
SKU詳細(xì)信息內(nèi)容不能為空
//Google Play應(yīng)用內(nèi)結(jié)算v3,部分三星手機上出現(xiàn)異常疲牵,
//崩潰日志結(jié)果與調(diào)用的BillingClient.onBillingServiceDisconnected()有關(guān)承二。
java.lang.IllegalStateException:
at android.os.Parcel.createException (Parcel.java:2096)
at android.os.Parcel.readException (Parcel.java:2056)
at android.os.Parcel.readException (Parcel.java:2004)
at android.app.IActivityManager$Stub$Proxy.registerReceiver (IActivityManager.java:5557)
at android.app.ContextImpl.registerReceiverInternal (ContextImpl.java:1589)
at android.app.ContextImpl.registerReceiver (ContextImpl.java:1550)
at android.app.ContextImpl.registerReceiver (ContextImpl.java:1538)
at android.content.ContextWrapper.registerReceiver (ContextWrapper.java:641)
at com.android.billingclient.api.zze.zza (zze.java:5)
at com.android.billingclient.api.zzd.zza (zzd.java:5)
at com.android.billingclient.api.BillingClientImpl.startConnection (BillingClientImpl.java:58)
at de.memorian.gzg.presentation.base.IAPHelper.initBilling (IAPHelper.java:40)
at de.memorian.gzg.presentation.base.IAPHelper$initBilling$1.onBillingServiceDisconnected (IAPHelper.java:53)
at com.android.billingclient.api.BillingClientImpl$zza.onServiceDisconnected (BillingClientImpl.java:11)
at android.app.LoadedApk$ServiceDispatcher.doConnected (LoadedApk.java:2060)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run (LoadedApk.java:2099)
at android.os.Handler.handleCallback (Handler.java:883)
at android.os.Handler.dispatchMessage (Handler.java:100)
at android.os.Looper.loop (Looper.java:237)
at android.app.ActivityThread.main (ActivityThread.java:7857)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1076)
Caused by: android.os.RemoteException:
at com.android.server.am.ActivityManagerService.registerReceiver (ActivityManagerService.java:16726)
at android.app.IActivityManager$Stub.onTransact (IActivityManager.java:2250)
at com.android.server.am.ActivityManagerService.onTransact (ActivityManagerService.java:3357)
at android.os.Binder.execTransactInternal (Binder.java:1021)
at android.os.Binder.execTransact (Binder.java:994)
Google play 結(jié)算庫一次性交易的生命周期
向用戶展示他們可以購買什么。
啟動購買流程纲爸,以便用戶接受購買交易亥鸠。
可以在你的服務(wù)器上驗證購買交易。
向用戶提供內(nèi)容识啦,并確認(rèn)內(nèi)容已傳送給用戶读虏。還可以選擇性地將商品標(biāo)記為已消費,以便用戶可以再次購買商品袁滥。
Google play 結(jié)算庫集成步驟
先給大家概念的解釋一下什么是一次性商品和 訂閱類型商品
BillingClient.SkuType.INAPP : 針對一次性商品,通過用戶的付款方式重復(fù)持續(xù)性質(zhì)的購買灾螃,也稱為內(nèi)購
一次性商品: 可多次購買题翻,例如:6美元 10K金幣
非消耗類型商品: 只能購買一次就能永久使用的商品, 例如:關(guān)卡包升到對應(yīng)等級只能購買一次
BillingClient.SkuType.SUBS : 訂閱是一種讓用戶定期使用內(nèi)容的商品腰鬼。訂閱期結(jié)束后嵌赠,訂閱會自動續(xù)訂,并且會通過用戶的付款方式向用戶另行收取費用熄赡。訂閱會無限期續(xù)訂姜挺,直到被取消。訂閱的示例包括在線雜志瀏覽和音樂在線播放服務(wù)等等彼硫。
- 例如購買某雷會員炊豪,周期性購買凌箕,你可以按月支付、也可以按季度词渤、和年來支付牵舱。
1 . 初始化BillingClient
BillingClient 是 Google Play 結(jié)算庫與應(yīng)用的其余部分之間進行通信的主接口
private BillingClient billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
2 . 與Google Play 建立連接
連接到Google pay的過程是異步的,所以是需要使用BillingClientStateListener監(jiān)聽缺虐,確保能夠成功的連接到Google Play
注意:請確保在執(zhí)行任何方法時都與 BillingClient 保持連接芜壁。
billingClient.startConnection(new BillingClientStateListener() {
//異步連接,接收結(jié)果回調(diào)高氮,連接成功了進行下一步邏輯操作
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
}
}
//重試邏輯慧妄,主要處理Google Pay失去連接的情況
@Override
public void onBillingServiceDisconnected() {
//嘗試在下次請求時重啟連接
//谷歌通過調(diào)用startConnection()方法進行連接
}
});
3 . 展示可購買的商品
前置條件完成,已與Google play建立連接后剪芍,可以查詢可售商品塞淹,需要調(diào)用異步詳細(xì)信息接口querySkuDetailsAsync()。querySkuDetailsAsync()會返回本地化的商品信息
onSkuDetailsResponse回調(diào)會將查詢到的商品信息存儲在列表字段SkuDetails對象里面紊浩,SkuDetails對象可以展示商品相關(guān)信息例如價格窖铡、商品id 等
List<String> skuList = new ArrayList<> ();
skuList.add("premium_upgrade");
skuList.add("gas");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// 去處理結(jié)果
}
});
4 . 啟動購買流程(拉起支付,可見支付UI)
先調(diào)用querySkuDetailsAsync()獲取"skuDetails"的值, 創(chuàng)建BillingFlowParams對象坊谁。通過billingClient.launchBillingFlow拉起Google 支付UI界面费彼,responseCode等于0 即表示打開UI成功 ,要對其他的狀態(tài)進行邏輯處理。
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();
//responseCode狀態(tài)進行邏輯判斷
5. 處理購買結(jié)果
在用戶退出 Google Play 購買界面時 (點擊 "購買" 按鈕完成購買口芍,或者點擊 "返回" 按鈕取消購買)箍铲,onPurchaseUpdated() 回調(diào)會將購買流程的結(jié)果發(fā)送回你的應(yīng)用。
然后鬓椭,根據(jù) BillingResult.responseCode 即可確定用戶是否成功購買產(chǎn)品颠猴。如果 responseCode == OK,則表示購買已成功完成小染。同時也要對其他的狀態(tài)進行邏輯處理翘瓮。
onPurchaseUpdated() 會傳回一個 Purchase 對象列表,其中包括用戶通過應(yīng)用進行的所有購買裤翩。每個 Purchase 對象都包含 sku资盅、purchaseToken 和 isAcknowledged 以及其他很多字段。使用這些字段踊赠,你可以確定每個 Purchase 對象是需要處理的新購買還是不需要進一步處理的既有購買呵扛。
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
6 . 驗證和確認(rèn)購買
購買成功后,完成購買流程筐带。如果應(yīng)用未在 72 小時內(nèi)確認(rèn)購買今穿,則用戶會自動收到退款,并且 Google Play 會撤消該購買交易伦籍。這一步建議拿PurchaseToken等信息 去服務(wù)器做一下校驗蓝晒,防止信息被篡改腮出。
購買進行驗證之后,還需要對其進行確認(rèn)拔创。consumeAsync()是處理消耗性商品利诺,用來標(biāo)記為 "已消耗 (consumed)",使得用戶可以再次購買剩燥。
//消耗性商品 處理調(diào)用
void handlePurchase(Purchase purchase) {
//從BillingClient#queryPurchasesAsync或你的PurchasesUpdatedListener檢索購買慢逾。
Purchase purchase = ...;
//驗證購買。確保此purchaseToken的授權(quán)尚未被授予灭红。授予用戶權(quán)限侣滩。
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
//處理消費操作成功。
}
}
};
billingClient.consumeAsync(consumeParams, listener);
}
Google play 結(jié)算接入前置需要
提供一個空包(新建一個空白可運行工程即可)占位置变擒,然后提供給運營(正常一點的公司都應(yīng)該是這個流程)君珠,其中重點是applicationId 和.keystore文件這兩個文件,提交的versionName建議從1.0.0開始每次提版本+1即可娇斑。
applicationId是不可隨意更改策添,它與參數(shù) 配置等信息綁定。簽名文件(keystore)是否正確取決于你的支付是否能被正常拉起毫缆。拿到程序給的包體后唯竹,運營把包體提交谷歌后臺用于申請參數(shù)。
參數(shù)申請完成后運營會在Google后臺結(jié)算頁面配置商品id 等信息(不細(xì)說了苦丁,多了我也不知道浸颓,沒配置過)一定要配置、一定要配置旺拉、不配置的話支付拉不起产上。
上述的前置條件弄好后,先準(zhǔn)備一個穩(wěn)定科學(xué)上網(wǎng)的工具蛾狗,這個很重要這個決定是否連接上谷歌服務(wù)晋涣。測試機最好是用海外的手機例如goole pixel3,千萬別用華為手機沉桌,谷歌服務(wù)連接在華為手機上是被默認(rèn)閹割禁止掉的谢鹊。
申請一個谷歌賬號,然后讓運營把你的谷歌賬號添加為開發(fā)者蒲牧。因為我添加了開發(fā)者可以不需要真實支付,所以 谷歌賬號并沒有綁定海外銀行卡 赌莺。程序這塊需要運營提供一個google-services.json文件用于支付冰抢。
Google play 接入前需要的代碼環(huán)境配置
//最外層的build.gradle 需要加上Google 服務(wù)插件
apply from:"config.gradle"
buildscript {
repositories { //配置遠(yuǎn)程倉庫
google()
jcenter()
}
dependencies { //配置構(gòu)建工具
classpath "com.android.tools.build:gradle:4.1.2"
classpath 'com.google.gms:google-services:4.3.3' // Google Services plugin
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
/**
* 運行g(shù)radle clean時,執(zhí)行此處定義的task任務(wù)艘狭。
*/
task clean(type: Delete) {
delete rootProject.buildDir
}
//添加谷歌服務(wù)依賴
apply plugin: 'com.google.gms.google-services' // Google Services plugin
//向 app/build.gradle 文件中添加依賴關(guān)系
implementation 'com.android.billingclient:billing:4.0.0'
google-services.json文件存放的位置
Google Play結(jié)算代碼部分
說明:代碼中xxxx_coins_1.99為我司游戲挎扰,真實的谷歌后臺配置其中一個計費點翠订,為了避免不必要的麻煩用xxx代替,如果需要看demo支付結(jié)算代碼流程遵倦,需要更換成使用者真實配置的商品id尽超,真實開發(fā)中 計費點不可能為一個。下面demo為了演示梧躺,所以寫死商品id(xxxx_coins_1.99)
package com.example.myapplication;
import android.app.Activity;
import android.os.Bundle;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MainActivity extends Activity implements PurchasesUpdatedListener {
public static final String TAG="MainActivity";
private Button btnGooglePay;
private BillingClient billingClient;
private BillingFlowParams billingFlowParams;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
billingClientiCreate();
btnGooglePay=findViewById(R.id.btnGooglePay);
btnGooglePay.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
launchBillingFlow(MainActivity.this,billingFlowParams);
}
});
}
/**
* 初始化創(chuàng)建BillingClient對象
* isReady() :檢查客戶端當(dāng)前是否連接到服務(wù)似谁,以便對其他方法的請求將成功。
*/
private void billingClientiCreate(){
LogUtils.i(TAG, "BillingClienticreate");
//在 onCreate() 中創(chuàng)建一個新的 BillingClient掠哥。由于 BillingClient 只能使用一次巩踏,因此我們需要在 onDestroy() 中結(jié)束之前與 Google Play 商店的連接后創(chuàng)建一個新實例
billingClient = BillingClient.newBuilder(MainActivity.this)
.setListener(this)
.enablePendingPurchases() // 非訂閱
.build();
if (!billingClient.isReady()) {
LogUtils.i(TAG, "BillingClient: Start connection...");
billingClient.startConnection(new BillingClientStateListener(){
@Override
public void onBillingServiceDisconnected() { //計費服務(wù)已斷開連接
LogUtils.i(TAG, "onBillingServiceDisconnected");
Toast.makeText(MainActivity.this,"計費服務(wù)已斷開連接,請檢查一下網(wǎng)絡(luò)是否有誤",Toast.LENGTH_LONG).show();
}
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
LogUtils.i(TAG, "onBillingSetupFinished:==== " +"responseCode:===="+ responseCode + "==========" + debugMessage);
if (responseCode == BillingClient.BillingResponseCode.OK) { // //計費客戶端已準(zhǔn)備就緒续搀。您可以在此處查詢購買情況
querySkuDetails();
queryPurchases();
}
}
});
}
}
/**
* 查詢已經(jīng)購買過但是沒有被消耗的商品塞琼,可能網(wǎng)絡(luò)不穩(wěn)定或者中斷導(dǎo)致的未被消耗
* 如果購買成功沒消耗,就去消耗禁舷,消耗完成視為完整的流程彪杉。
*/
public void queryPurchases() {
if (!billingClient.isReady()) {
LogUtils.i(TAG, "queryPurchases: BillingClient is not ready");
}
LogUtils.i(TAG, "queryPurchases: INAPP");
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, new PurchasesResponseListener(){
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> purchasesList) {
if (purchasesList != null) {
LogUtils.i(TAG, "processPurchases: " + purchasesList.size() + " purchase(s)");
for (int i = 0; i < purchasesList.size(); i++) {
Purchase purchase = purchasesList.get(i);
handlePurchase(purchase);
}
} else {
LogUtils.i(TAG, "processPurchases: with no purchases");
}
}
});
}
/**
* 查詢 Sku 詳情
*/
public void querySkuDetails() {
LogUtils.i(TAG, "querySkuDetails");
List<String> skuList=new ArrayList<>();
skuList.add("xxx_coins_1.99"); //productId 谷歌后臺配置的一次消耗類型商品ID
SkuDetailsParams params = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.INAPP)
.setSkusList(skuList)
.build();
LogUtils.i(TAG, "querySkuDetailsAsync");
billingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener(){ //從 querySkuDetailsAsync 接收結(jié)果,來顯示 SKU 信息并進行購買牵咙。
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> skuDetailsList) {
if (billingResult == null) {
LogUtils.i(TAG, "onSkuDetailsResponse: null BillingResult");
return;
}
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
LogUtils.i(TAG, "onSkuDetailsResponse:" +"responseCode:====="+ responseCode + "==========" + debugMessage);
switch (responseCode) {
case BillingClient.BillingResponseCode.OK:
LogUtils.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
final int expectedSkuDetailsCount = skuList.size();
//如果SkuDetails為空派近,應(yīng)該檢查你請求的 SKU 是否在 Google Play Console 中正確發(fā)布
if (skuDetailsList == null) {
LogUtils.i(TAG, "onSkuDetailsResponse: " +
"Expected " + expectedSkuDetailsCount + ", " +
"Found null SkuDetails. " +
"Check to see if the SKUs you requested are correctly published " +
"in the Google Play Console.");
} else {
Map<String, SkuDetails> newSkusDetailList = new HashMap<String, SkuDetails>();
for (SkuDetails skuDetails : skuDetailsList) {
newSkusDetailList.put(skuDetails.getSku(), skuDetails);
String sku = skuDetails.getSku();//商品ID
LogUtils.i(TAG,"獲取到的商品ID====="+sku);
if ("xxxx_coins_1.99".equals(sku)){
LogUtils.i(TAG, skuDetails.toString());
billingFlowParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build();
}
}
if (newSkusDetailList.size() == expectedSkuDetailsCount) {
LogUtils.i(TAG, "onSkuDetailsResponse: Found " + newSkusDetailList.size() + " SkuDetails");
} else {
LogUtils.i(TAG, "onSkuDetailsResponse: " +
"Expected " + expectedSkuDetailsCount + ", " +
"Found " + newSkusDetailList.size() + " SkuDetails. " +
"Check to see if the SKUs you requested are correctly published " +
"in the Google Play Console.");
}
}
break;
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
case BillingClient.BillingResponseCode.ERROR:
LogUtils.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
LogUtils.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
break;
// These response codes are not expected.
case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
default:
LogUtils.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
}
}
});
}
/**
* 啟動計費流程。 <p> 啟動 UI 進行購買需要對 Activity 的引用霜大。
*/
public int launchBillingFlow(Activity activity, BillingFlowParams params) {
if (!billingClient.isReady()) {
LogUtils.i(TAG, "launchBillingFlow: BillingClient is not ready");
}
BillingResult billingResult = billingClient.launchBillingFlow(activity, params);
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
LogUtils.i(TAG, "launchBillingFlow: BillingResponse " + responseCode + " " + debugMessage);
return responseCode;
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy");
if (billingClient.isReady()) {
Log.d(TAG, "BillingClient can only be used once -- closing connection");
//BillingClient 只能使用一次构哺。
// 在調(diào)用 endConnection() 之后,我們必須創(chuàng)建一個新的 BillingClient战坤。
billingClient.endConnection();
}
}
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
if (billingResult == null) {
LogUtils.i(TAG, "onPurchasesUpdated: null BillingResult");
return;
}
int responseCode = billingResult.getResponseCode();
String debugMessage = billingResult.getDebugMessage();
LogUtils.i(TAG, String.format("onPurchasesUpdated: %s %s",responseCode, debugMessage));
switch (responseCode) {
case BillingClient.BillingResponseCode.OK:
if (purchases != null) {
for (Purchase purchase :purchases){
handlePurchase(purchase); //去消耗掉
}
} else{
LogUtils.i(TAG, "onPurchasesUpdated: null purchase list");
}
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
LogUtils.i(TAG, "onPurchasesUpdated: User canceled the purchase");
break;
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
LogUtils.i(TAG, "onPurchasesUpdated: The user already owns this item");
break;
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
LogUtils.i(TAG, "onPurchasesUpdated: Developer error means that Google Play " +
"does not recognize the configuration. If you are just getting started, " +
"make sure you have configured the application correctly in the " +
"Google Play Console. The SKU product ID must match and the APK you " +
"are using must be signed with release keys."
);
break;
}
}
/**
* 處理消耗商品邏輯
* @param purchase
*/
private void handlePurchase(Purchase purchase) {
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//處理消耗成功的邏輯,正常purchaseToken 游戲生成的訂單號曙强, 等服務(wù)器需要的信息,去游戲服務(wù)器去驗證途茫,
//游戲服務(wù)器再去google服務(wù)器驗證碟嘴,驗證成功后,通知游戲服務(wù)器囊卜,游戲服務(wù)器通知客戶端下發(fā)道具娜扇。
LogUtils.i(TAG, "onConsumeResponse 商品購買成功,下發(fā)道具======" +purchaseToken);
}
}
};
billingClient.consumeAsync(consumeParams, listener); //去消耗道具
}
}
Purchase對象屬性
skuDetails里面的屬性
{
"productId": "xxxx_coins_1.99",
"type": "inapp",
"price": "HK$15.00",
"price_amount_micros": 15000000,
"price_currency_code": "HKD",
"title": "這是一個title,在google后臺配置的",
"description": "這是一個商品描述栅组,在google后臺配置的",
"skuDetailsToken": "qw1e21312rsdghh235hgsagh"
}