接下来给大家介绍下Google billing储值库的接入,以及如何避坑!!
Google billing接入文档:https://developer.android.com/google/play/billing/integrate?hl=zh-cn
下面将以billing5.2.1版本示例
一、调用流程
1.配置
dependencies {
def billing_version = "5.2.1"
implementation "com.android.billingclient:billing:$billing_version"
}
如果需要手动集成,不想使用maven方式,可以使用 fat-aar 把库拉下来
2.创建 PurchasesUpdatedListener
private static PurchasesUpdatedListener getListener(final Context context) {
return new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(final BillingResult billingResult, @Nullable final List<Purchase> list) {
LogUtil.i(TAG, "onPurchasesUpdated: " + billingResult.getDebugMessage());
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
final ArrayList<Pur> results = new ArrayList<>();
if (list != null && !list.isEmpty()) {
for (Purchase purchase : list) {
try {
Pur pur = new Pur(purchase.getOriginalJson(), purchase.getSignature());
results.add(pur);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
if (!results.isEmpty()) {
ArrayList<QueryProductDetailsParams.Product> productArrayList = new ArrayList<>();
for (Pur p : results) {
productArrayList.add(QueryProductDetailsParams.Product.newBuilder()
.setProductId(p.getSku())
.setProductType(BillingClient.ProductType.INAPP)
.build());
}
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(productArrayList)
.build();
mBillingClient.queryProductDetailsAsync(productDetailsParams, new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(final BillingResult billingResult, final List<ProductDetails> list) {
LogUtil.i(TAG, "onProductDetailsResponse: " + billingResult.getDebugMessage());
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (list != null && !list.isEmpty()) {
for (ProductDetails details : list) {
for (Pur p : results) {
//处理储值数据
}
}
} else {
}
} else {
}
}
});
}
});
} else {
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
LogUtil.w(TAG, "USER_CANCELED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE) {
LogUtil.w(TAG, "ITEM_UNAVAILABLE");
} else {
LogUtil.e(TAG, "onPurchasesUpdated not OK: " + billingResult.getResponseCode());
}
}
});
}
};
}
3.初始化 BillingClient
connection.connected();
connection.disConnection(str);
回调接口interface请自行实现
private static BillingClient mBillingClient;
private static void connection(Context context,回调){
if (mListener == null) {
mListener = getListener(context);
}
mBillingClient = BillingClient.newBuilder(context).enablePendingPurchases().setListener(mListener).build();
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(final BillingResult billingResult) {
LogUtil.i(TAG, "onBillingSetupFinished: " + billingResult.getDebugMessage());
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
connection.connected();
} else {
LogUtil.w(TAG, "onBillingSetupFinished: connection result is not OK");
LogUtil.w(TAG, "onBillingSetupFinished ResponseCode: " + billingResult.getResponseCode());
connection.disConnection("onBillingSetupFinished ResponseCode: " + billingResult.getResponseCode());
}
}
});
}
@Override
public void onBillingServiceDisconnected() {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
LogUtil.w(TAG, "onBillingServiceDisconnected: Billing Service Disconnected");
connection.disConnection("Billing Service Disconnected");
}
});
}
});
}
4.查询应用内商品详情
connection(context, new Connection() {
@Override
public void connected() {
ArrayList<QueryProductDetailsParams.Product> productList = new ArrayList<>();
for (int i = 0; i < skuList.size(); i++) {
productList.add(QueryProductDetailsParams.Product.newBuilder()
.setProductId(skuList.get(i))
.setProductType(BillingClient.ProductType.INAPP)
.build());
}
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
mBillingClient.queryProductDetailsAsync(productDetailsParams, new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(final BillingResult billingResult, final List<ProductDetails> productDetailsList) {
LogUtil.i(TAG, "onSkuDetailsResponse: " + billingResult.getDebugMessage());
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
ArrayList<Sku> results = new ArrayList<>();
if (productDetailsList != null && !productDetailsList.isEmpty()) {
for (ProductDetails details : productDetailsList) {
//处理数据results.add()
}
}
} else {
LogUtil.w(TAG, "onSkuDetailsResponse: Response Code is not OK");
LogUtil.w(TAG, "onSkuDetailsResponse ResponseCode: " + billingResult.getResponseCode());
}
}
});
}
}
);
}
@Override
public void disConnection(String msg) {
}
});
5.查询最近的购买交易
如果存在掉单的,可以获取到对应商品的信息,然后进行补单
connection(context, new Connection() {
@Override
public void connected() {
QueryPurchaseHistoryParams historyParams = QueryPurchaseHistoryParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build();
mBillingClient.queryPurchaseHistoryAsync(historyParams, new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(final BillingResult billingResult, List<PurchaseHistoryRecord> list) {
LogUtil.i(TAG, "onPurchaseHistoryResponse: " + billingResult.getDebugMessage());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
QueryPurchasesParams purchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build();
mBillingClient.queryPurchasesAsync(purchasesParams, new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(final BillingResult billingResult, final List<Purchase> purchaseList) {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
ArrayList<Pur> results = new ArrayList<>();
if (purchaseList != null && !purchaseList.isEmpty()) {
for (Purchase purchase : purchaseList) {
//掉单数据,回调出去处理
results.add(pur);
}
}
historyListener.result(results);
} else {
LogUtil.w(TAG, "purchasesResult: Response Code is not OK");
}
}
});
}
});
} else {
}
}
});
}
@Override
public void disConnection(String msg) {
}
});
6.拉起储值
connection(activity, new Connection() {
@Override
public void connected() {
ArrayList<QueryProductDetailsParams.Product> inAppProductInfo = new ArrayList<>();
inAppProductInfo.add(QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP)
.build());
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(inAppProductInfo)
.build();
mBillingClient.queryProductDetailsAsync(productDetailsParams, new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(final BillingResult billingResult, final List<ProductDetails> list) {
LogUtil.i(TAG, "onSkuDetailsResponse: " + billingResult.getDebugMessage());
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (list != null && !list.isEmpty()) {
for (final ProductDetails details : list) {
LogUtil.i(TAG, "onSkuDetailsResponse: " + details.toString());
if (productId.equals(details.getProductId())) {
/**
* 优惠购买流程参数:OfferToken
* 正常购买流程这个参数没有返回的,但是可能在某些优惠购买中需要使用这个参数
*/
BillingFlowParams.ProductDetailsParams.Builder newBuilder = BillingFlowParams.ProductDetailsParams.newBuilder();
newBuilder.setProductDetails(details);
List<ProductDetails.SubscriptionOfferDetails> offerDetailsList = details.getSubscriptionOfferDetails();
if (offerDetailsList != null && offerDetailsList.size() > 0) {
for (ProductDetails.SubscriptionOfferDetails offerDetails : offerDetailsList) {
newBuilder.setOfferToken(offerDetails.getOfferToken());
}
}
ArrayList<BillingFlowParams.ProductDetailsParams> paramsArrayList = new ArrayList<>();
paramsArrayList.add(newBuilder.build());
BillingFlowParams.Builder builder = BillingFlowParams.newBuilder();
builder.setProductDetailsParamsList(paramsArrayList);
builder.setObfuscatedAccountId(obfuscatedAccountId);
builder.setObfuscatedProfileId(obfuscatedProfileId);
mBillingClient.launchBillingFlow(activity, builder.build());
break;
}
}
} else {
LogUtil.w(TAG, "onSkuDetailsResponse: sku is not support");
}
} else {
LogUtil.w(TAG, "onSkuDetailsResponse: Response code is not ok");
}
}
});
}
});
}
@Override
public void disConnection(String msg) {
}
});
7.确认购买
connection(context, new Connection() {
@Override
public void connected() {
ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(pur.getPurchaseToken()).build();
mBillingClient.consumeAsync(params, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(final BillingResult billingResult, final String outToken) {
}
});
}
@Override
public void disConnection(String msg) {
LogUtil.w(TAG, "consume disConnection: " + msg);
}
});
8.断开连接
if (mBillingClient != null) {
mBillingClient.endConnection();
}
9.注意事项
1.建议在储值同一品项时触发掉单补单流程,补单完成后再拉起新的订单,具体的补单流程,创单流程自行完善!
2.billing5后,查询接口返回的数据格式和billing3、4是不一样的,这个一定要注意!!
billing3:
{
"productId": "test.1usd",
"type": "inapp",
"title": "一钻 (test)",
"name": "一钻",
"description": "一钻",
"price": "HK$9.90",
"price_amount_micros": 9900000,
"price_currency_code": "HKD",
"skuDetailsToken": "*********************"
}
billing5:
{
"productId":"test.1usd",
"type":"inapp",
"title":"一钻 (test)",
"name":"一钻",
"description":"一钻",
"localizedIn":["zh-CN"],
"skuDetailsToken":"****************",
"oneTimePurchaseOfferDetails":{
"priceAmountMicros":9900000,
"priceCurrencyCode":"HKD",
"formattedPrice":"HK$9.90"}
}',
parsedJson={"productId":"test.1usd","type":"inapp","title":"一钻 (test)","name":"一钻","description":"一钻","localizedIn":["zh-CN"],
"skuDetailsToken":"**************",
"oneTimePurchaseOfferDetails":{"priceAmountMicros":9900000,"priceCurrencyCode":"HKD","formattedPrice":"HK$9.90"}},
"productId='test.1usd', productType='inapp', title='一钻 (test)', "
"productDetailsToken='*********', subscriptionOfferDetails=null}"
3.billing5后,会出现一个兼容的问题,就是在低版本的play商店中储值时会报错,导致无法储值,升级一下play商店的版本就行。
- 2023 年 8 月 2 日起,所有新应用都必须使用结算库版本 5 或更高版本。自 2023 年 11 月 1 日起,现有应用的所有新版本都必须使用结算库版本 5 或更高版本;billing6估计也在24年8月开始全面使用!!
二、预注册和积分兑换
预注册官方文档:https://developer.android.com/distribute/best-practices/launch/pre-registration?hl=zh-cn
积分兑换官方文档:https://developer.android.com/guide/playpoints/deliver-items?hl=zh-cn
预注册和积分兑换,以及测试时使用的兑换码兑换都是使用以下接口进行查询,具体怎么区分是哪个类型,请自行设计接口实现,通过枚举类传入区分,因为这部分涉及比较多的服务端逻辑,此处就不详细说明了
QueryPurchaseHistoryParams historyParams = QueryPurchaseHistoryParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build();
mBillingClient.queryPurchaseHistoryAsync(historyParams, new PurchaseHistoryResponseListener() {
@Override
public void onPurchaseHistoryResponse(final BillingResult billingResult, List<PurchaseHistoryRecord> list) {
LogUtil.i(TAG, "onPurchaseHistoryResponse: " + billingResult.getDebugMessage());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
QueryPurchasesParams purchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build();
mBillingClient.queryPurchasesAsync(purchasesParams, new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(final BillingResult billingResult, final List<Purchase> purchaseList) {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
ArrayList<Pur> results = new ArrayList<>();
if (purchaseList != null && !purchaseList.isEmpty()) {
for (Purchase purchase : purchaseList) {
//获取数据处理,回调出去
}
}
} else {
}
}
});
}
});
} else {
}
}
});
2.处理
//回调中处理逻辑,示例
/**
* 注意:
* 兑换码兑换时(【预注册】和【积分兑换】)pru.getOrderId()是没有返回/返回是空的
* 正式时【积分兑换】pru.getOrderId()是有值返回的
* 【预注册】正式时还没验证,暂保留原来逻辑不变
*/
//预注册,兑换码
if (PRE_REGISTRATION.equals(type) || PROMO_CODE.equals(type)) {
if (TextUtils.isEmpty(pru.getOrderId())) {
}
} else if (REDEEM.equals(type)) {
//积分兑换
}
//请求贵方服务端进行确认
逻辑省略.....
//服务端确认完成,可发奖后,通知移动端,移动端再请求服务端发奖
//发奖完成后,由客户端通知Google,确定商品
ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(pur.getPurchaseToken()).build();
mBillingClient.consumeAsync(params, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(final BillingResult billingResult, final String outToken) {
}
});
预注册:
积分兑换:
至此,预注册和积分兑换就完成了!!
其它常见的问题还没有整理好,后续再完善更新!!感谢~~