前言
最近看群聊,一位兄弟去面试安卓SDK岗位,面试时候被问到了google play结算流程。这位兄弟平时主要是负责国内的SDK渠道,海外SDK基本是没有了解。
结果面试过程一脸尴尬,面完后在群里也分享了一下面试过程,正好最近公司要更新一下google play 结算库 4.0,顺便我做个分享,希望群里哪位兄弟能看见 。
了解一下最近几个版本结算库的变化
Google Play 结算库 4.0 版 (2021-05-18)
当前最新版本,变更内容
添加了 BillingClient.queryPurchasesAsync() 以替换 BillingClient.queryPurchases(),我们将在未来的版本中移除后者。
添加了新的订阅替换模式 IMMEDIATE_AND_CHARGE_FULL_PRICE。
添加了 BillingClient.getConnectionState() 方法,用于检索 Play 结算库的连接状态。
更新了 Javadoc 和实现,用于指明可在哪个线程上调用方法以及发布哪些线程结果。
-
添加了 BillingFlowParams.Builder.setSubscriptionUpdateParams() 作为发起订阅更新的新方式,用于替换已移除的
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 结算库 3.0.3 版 (2021-03-12)
修复了在调用 endConnection() 时发生内存泄漏的问题。
修复了利用单个任务启动模式的应用在使用 Google Play 结算库时出现的问题。当应用从 Android 启动器恢复运行且结算对话框在暂停之前可见时,将触发 onPurchasesUpdated() 回调。
Unity 问题修复
更新到了 Java 3.0.3,解决了内存泄漏问题,并解决了当应用从 Android 启动器恢复运行且结算对话框在暂停之前可见时出现的无法购买的问题。
Google Play 结算库 3.0.2 版 (2020-11-24)
问题修复
修复了 Kotlin 扩展程序中的一个错误:协程失败并显示错误“Already resumed”。
修复了将 Kotlin 扩展程序与 kotlinx.coroutines 库版本 1.4 及更高版本一起使用时未解析的引用。
Google Play 结算库 3.0.1 版 (2020-09-30)
问题修复
- 修复了以下错误:如果在结算过程中终止后恢复应用,系统可能不会使用购买结果调用 PurchasesUpdatedListener。
Google Play 结算库 4.0版需要关心的事情
根据需求,我司游戏采用了一次性商品模式,订阅模式相关不作说明,本文着重讲解一次性商品模式,往下看我会解释订阅和一次性商品两种模式的概念
一次性模式建议用BillingClient.queryPurchasesAsync(),BillingClient.queryPurchases()后续会被删除掉。此方法官方描述:异步操作,返回活动订阅和非消费的一次性购买。
这个方法使用谷歌Play Store应用的缓存,而不需要发起网络请求。注:建议购服务端通过调用下面文档接口做安全验证
BillingClient.getConnectionState()方法,用于检索 Play 结算库的连接状态。这个方法我查一下官方Demo并没有使用,Google搜索也并没有使用对应blog,暂不建议使用。
新增两个方法
Purchase.getQuantity() :表示应用内付费购买。返回购买的商品数量 ,订阅方式 固定返回1、一次性商品方式大于1
PurchaseHistoryRecord.getQuantity():应用内付费购买历史记录,返回购买的商品数量 ,订阅方式 固定返回1 、一次性商品方式大于1
Purchase.getSku()被Purchase.getSkus() 代替了,getSku()方法被删除掉 。
还有一个重点 endConnection() 升级到4.x修复了内存泄漏的问题。
BillingClient重要说明
BillingClient用于库和用户应用程序代码之间通信的主界面。为应用内计费提供了便利的方法。应用程序创建该类的一个实例,并使用它来处理应用程序内的计费操作。它为许多常见的应用内计费操作提供了同步(阻塞)和异步(非阻塞)方法。
强烈建议一次只实例化一个BillingClient实例,以避免多个PurchasesUpdatedListener。onPurchasesUpdated(BillingResult, List) 回调单个事件。
所有带AnyThread注释的方法都可以从任何线程调用,所有异步回调都将在同一个线程上返回。用UiThread注释的方法应该从Ui(主)线程调用,所有的异步回调也将在Ui(主)线程上返回。
实例化后,必须执行设置才能开始使用该对象。要执行设置,调用startConnection(BillingClientStateListener)方法并提供一个监听器;当完成时,该监听器将得到通知,在此之后(而不是之前),你可以开始调用其他方法。
当你使用完这个对象时,不要忘记调用endConnection()以确保正确的清理该对象。该对象持有与应用内计费服务和管理器的绑定,用于处理广播事件,除非你能正确地处理它,否则广播事件将泄漏。
如果你在Activity.onCreate(Bundle)方法中创建了对象,那么建议将endConnection()方法放到Activity.onDestroy()方法中。清理后,不能再为连接重用它。
如何处理BillingClient.onBillingServiceDisconnected()
关于是否在Google服务断开后重连的问题,建议通过回调让游戏做提示,不做重连逻辑。在4.0官方demo中注释写到尝试再次连接服务 ,其实是什么都没有做的。
在查阅一番资料在v3.0结算库断开后重新连接,可能会出现异常闪退的问题。4.0中每次购买前可以做以下逻辑判断。
BillingClient是否初始化成功
BillingClient 是否 isReady()
SKU详细信息内容不能为空
//Google Play应用内结算v3,部分三星手机上出现异常,
//崩溃日志结果与调用的BillingClient.onBillingServiceDisconnected()有关。
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 结算库一次性交易的生命周期
向用户展示他们可以购买什么。
启动购买流程,以便用户接受购买交易。
可以在你的服务器上验证购买交易。
向用户提供内容,并确认内容已传送给用户。还可以选择性地将商品标记为已消费,以便用户可以再次购买商品。
Google play 结算库集成步骤
先给大家概念的解释一下什么是一次性商品和 订阅类型商品
BillingClient.SkuType.INAPP : 针对一次性商品,通过用户的付款方式重复持续性质的购买,也称为内购
一次性商品: 可多次购买,例如:6美元 10K金币
非消耗类型商品: 只能购买一次就能永久使用的商品, 例如:关卡包升到对应等级只能购买一次
BillingClient.SkuType.SUBS : 订阅是一种让用户定期使用内容的商品。订阅期结束后,订阅会自动续订,并且会通过用户的付款方式向用户另行收取费用。订阅会无限期续订,直到被取消。订阅的示例包括在线杂志浏览和音乐在线播放服务等等。
- 例如购买某雷会员,周期性购买,你可以按月支付、也可以按季度、和年来支付。
1 . 初始化BillingClient
BillingClient 是 Google Play 结算库与应用的其余部分之间进行通信的主接口
private BillingClient billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
2 . 与Google Play 建立连接
连接到Google pay的过程是异步的,所以是需要使用BillingClientStateListener监听,确保能够成功的连接到Google Play
注意:请确保在执行任何方法时都与 BillingClient 保持连接。
billingClient.startConnection(new BillingClientStateListener() {
//异步连接,接收结果回调,连接成功了进行下一步逻辑操作
@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() {
//尝试在下次请求时重启连接
//谷歌通过调用startConnection()方法进行连接
}
});
3 . 展示可购买的商品
前置条件完成,已与Google play建立连接后,可以查询可售商品,需要调用异步详细信息接口querySkuDetailsAsync()。querySkuDetailsAsync()会返回本地化的商品信息
onSkuDetailsResponse回调会将查询到的商品信息存储在列表字段SkuDetails对象里面,SkuDetails对象可以展示商品相关信息例如价格、商品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) {
// 去处理结果
}
});
4 . 启动购买流程(拉起支付,可见支付UI)
先调用querySkuDetailsAsync()获取"skuDetails"的值, 创建BillingFlowParams对象。通过billingClient.launchBillingFlow拉起Google 支付UI界面,responseCode等于0 即表示打开UI成功 ,要对其他的状态进行逻辑处理。
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();
//responseCode状态进行逻辑判断
5. 处理购买结果
在用户退出 Google Play 购买界面时 (点击 "购买" 按钮完成购买,或者点击 "返回" 按钮取消购买),onPurchaseUpdated() 回调会将购买流程的结果发送回你的应用。
然后,根据 BillingResult.responseCode 即可确定用户是否成功购买产品。如果 responseCode == OK,则表示购买已成功完成。同时也要对其他的状态进行逻辑处理。
onPurchaseUpdated() 会传回一个 Purchase 对象列表,其中包括用户通过应用进行的所有购买。每个 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 . 验证和确认购买
购买成功后,完成购买流程。如果应用未在 72 小时内确认购买,则用户会自动收到退款,并且 Google Play 会撤消该购买交易。这一步建议拿PurchaseToken等信息 去服务器做一下校验,防止信息被篡改。
购买进行验证之后,还需要对其进行确认。consumeAsync()是处理消耗性商品,用来标记为 "已消耗 (consumed)",使得用户可以再次购买。
//消耗性商品 处理调用
void handlePurchase(Purchase purchase) {
//从BillingClient#queryPurchasesAsync或你的PurchasesUpdatedListener检索购买。
Purchase purchase = ...;
//验证购买。确保此purchaseToken的授权尚未被授予。授予用户权限。
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 结算接入前置需要
提供一个空包(新建一个空白可运行工程即可)占位置,然后提供给运营(正常一点的公司都应该是这个流程),其中重点是applicationId 和.keystore文件这两个文件,提交的versionName建议从1.0.0开始每次提版本+1即可。
applicationId是不可随意更改,它与参数 配置等信息绑定。签名文件(keystore)是否正确取决于你的支付是否能被正常拉起。拿到程序给的包体后,运营把包体提交谷歌后台用于申请参数。
参数申请完成后运营会在Google后台结算页面配置商品id 等信息(不细说了,多了我也不知道,没配置过)一定要配置、一定要配置、不配置的话支付拉不起。
上述的前置条件弄好后,先准备一个稳定科学上网的工具,这个很重要这个决定是否连接上谷歌服务。测试机最好是用海外的手机例如goole pixel3,千万别用华为手机,谷歌服务连接在华为手机上是被默认阉割禁止掉的。
申请一个谷歌账号,然后让运营把你的谷歌账号添加为开发者。因为我添加了开发者可以不需要真实支付,所以 谷歌账号并没有绑定海外银行卡 。程序这块需要运营提供一个google-services.json文件用于支付。
Google play 接入前需要的代码环境配置
//最外层的build.gradle 需要加上Google 服务插件
apply from:"config.gradle"
buildscript {
repositories { //配置远程仓库
google()
jcenter()
}
dependencies { //配置构建工具
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()
}
}
/**
* 运行gradle clean时,执行此处定义的task任务。
*/
task clean(type: Delete) {
delete rootProject.buildDir
}
//添加谷歌服务依赖
apply plugin: 'com.google.gms.google-services' // Google Services plugin
//向 app/build.gradle 文件中添加依赖关系
implementation 'com.android.billingclient:billing:4.0.0'
google-services.json文件存放的位置
Google Play结算代码部分
说明:代码中xxxx_coins_1.99为我司游戏,真实的谷歌后台配置其中一个计费点,为了避免不必要的麻烦用xxx代替,如果需要看demo支付结算代码流程,需要更换成使用者真实配置的商品id,真实开发中 计费点不可能为一个。下面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);
}
});
}
/**
* 初始化创建BillingClient对象
* isReady() :检查客户端当前是否连接到服务,以便对其他方法的请求将成功。
*/
private void billingClientiCreate(){
LogUtils.i(TAG, "BillingClienticreate");
//在 onCreate() 中创建一个新的 BillingClient。由于 BillingClient 只能使用一次,因此我们需要在 onDestroy() 中结束之前与 Google Play 商店的连接后创建一个新实例
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() { //计费服务已断开连接
LogUtils.i(TAG, "onBillingServiceDisconnected");
Toast.makeText(MainActivity.this,"计费服务已断开连接,请检查一下网络是否有误",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) { // //计费客户端已准备就绪。您可以在此处查询购买情况
querySkuDetails();
queryPurchases();
}
}
});
}
}
/**
* 查询已经购买过但是没有被消耗的商品,可能网络不稳定或者中断导致的未被消耗
* 如果购买成功没消耗,就去消耗,消耗完成视为完整的流程。
*/
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 接收结果,来显示 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为空,应该检查你请求的 SKU 是否在 Google Play Console 中正确发布
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 只能使用一次。
// 在调用 endConnection() 之后,我们必须创建一个新的 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 游戏生成的订单号, 等服务器需要的信息,去游戏服务器去验证,
//游戏服务器再去google服务器验证,验证成功后,通知游戏服务器,游戏服务器通知客户端下发道具。
LogUtils.i(TAG, "onConsumeResponse 商品购买成功,下发道具======" +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"
}