安卓谷歌支付访问
在App的开发中,支付功能也是比较常见的,国内通常是集成支付宝和微信,但公司的App要上架GooglePlay,所以集成的是Google支付。本篇文章主要介绍一下如何接入Google支付。
之前在SDK项目中接入过4.1.0的Billing库,最近Billing库升级到了5.0.0,看到后我也是升级了。但是发现,如果手机上的GooglePlay商店版本太低,会导致无法正常使用新的API,例如无法正常获取商品信息。根据目前测试的情况来看,商店版本在30以下的都无法通过5.0.0的API获取商品信息。因此,本篇文章会同时介绍4.1.0和5.0.0两个版本的API。
官方文档传送门
集成Billing库
在项目app module的build.gradle中的dependencies中添加依赖:
dependencies {
//4.1.0和5.0.0选择一种即可
implementation("com.android.billingclient:billing:4.1.0")
implementation("com.android.billingclient:billing:5.0.0")
}
初始化与连接GooglePlay
这一部分两个版本之间的API没有变化,代码如下:
//购买交易更新监听,需要在初始化BillingClient时设置
//所有购买都会回调此监听
PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
switch (billingResult.getResponseCode()) {
case BillingClient.BillingResponseCode.OK:
//购买商品成功
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
//取消购买
break;
default:
//购买失败,具体异常码可以到BillingClient.BillingResponseCode中查看
break;
}
}
};
//初始化BillingClient
BillingClient billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
//支持待处理的交易
.enablePendingPurchases()
.build();
//与GooglePlay连接状态监听
BillingClientStateListener stateListener = new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//连接成功,可以进行查询商品等操作
}
}
@Override
public void onBillingServiceDisconnected() {
//连接已经断开,重新连接
billingClient.startConnection(this);
}
};
//与GooglePlay建立连接
billingClient.startConnection(stateListener);
ANR问题
在项目上线了一段时间后,收集到了一个ANR问题,从信息看是来自于onBillingServiceDisconnected
,查询了一下, 有人也遇到了相同的问题,判断是由于手机环境的问题导致onBillingServiceDisconnected
不断回调,startConnection
不断调用引起,所以我们可以给startConnection
加个间隔时间,代码如下:
private Handler handler = new Handler(Looper.myLooper() == null ? Looper.getMainLooper() : Looper.myLooper());
private Runnable retryConnectRunnable = new Runnable() {
@Override
public void run() {
if (BillingClient.ConnectionState.DISCONNECTED == billingClient.connectionState) {
billingClient.startConnection(stateListener);
}
}
};
BillingClientStateListener stateListener = new BillingClientStateListener() {
...
@Override
public void onBillingServiceDisconnected() {
handler.postDelayed(retryConnectRunnable, 1000);
}
};
获取可用的商品
Google支付的商品分为内购(INAPP)和订阅(SUBS)两种类型。获取商品的API在两个版本之间有所不同,下面分别介绍一下。
4.1.0 获取可用商品
代码如下:
//获取商品详情回调
SkuDetailsResponseListener skuDetailsResponseListener=new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
//list为可用商品的集合
}
};
//查询内购类型的商品
//productId为产品ID(从谷歌后台获取)
ArrayList<String> inAppSkuInfo = new ArrayList<>();
inAppSkuInfo.add("productId");
SkuDetailsParams skuParams = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.INAPP)
.setSkusList(inAppSkuInfo)
.build();
billingClient.querySkuDetailsAsync(skuParams, skuDetailsResponseListener);
//查询订阅类型的商品
//productId为产品ID(从谷歌后台获取)
ArrayList<String> subscriptionSkuInfo = new ArrayList<>();
subscriptionSkuInfo.add("productId");
SkuDetailsParams skuParams = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.SUBS)
.setSkusList(subscriptionSkuInfo)
.build();
billingClient.querySkuDetailsAsync(skuParams, skuDetailsResponseListener);
5.0.0 获取可用商品
代码如下:
ProductDetailsResponseListener productDetailsResponseListener = new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(@NonNull BillingResult billingResult, @NonNull List<ProductDetails> productDetailsList) {
//productDetailsList为可用商品的集合
}
};
//查询内购类型的商品
//设置查询参数方式有所更改,productId为产品ID(从谷歌后台获取)
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();
billingClient.queryProductDetailsAsync(productDetailsParams, productDetailsResponseListener);
//查询订阅类型的商品
//设置查询参数方式有所更改,productId为产品ID(从谷歌后台获取)
ArrayList<QueryProductDetailsParams.Product> subscriptionProductInfo = new ArrayList<>();
subscriptionProductInfo.add(QueryProductDetailsParams.Product.newBuilder()
.setProductId("productId")
.setProductType(BillingClient.ProductType.SUBS)
.build());
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(subscriptionProductInfo)
.build();
billingClient.queryProductDetailsAsync(productDetailsParams, productDetailsResponseListener);
商品价格
获取商品返回的集合中,商品的价格会根据所在的区域换算,但货币符号仍然为$。
例如上图中的商品,后台配置的价格为1.99美元。在测试机获取商品时,Google支付SDK判定我所在的地区是*省,所以返回的金额根据当时美元与台币的汇率进行了计算,但是返回的货币符号仍然是美元符号。如果直接用返回的价格显示,可能会对用户造成困扰。
从上面的图片中可以看到,返回的信息中有货币码(CurrencyCode),我们可以根据货币码来对价格进行优化。
Android SDK中提供了Currency
类,可以获取所有的货币码信息,代码如下:
//用map存储,便于匹配
ArrayMap<String, String> currencyArrayMap = new ArrayMap<>();
for (Currency availableCurrency : Currency.getAvailableCurrencies()) {
//currentcyCode为key,货币符号为value
//对于没有特定符号的货币,symbol与currencyCode相同。
currencyArrayMap.put(availableCurrency.getCurrencyCode(), availableCurrency.getSymbol());
}
获取商品价格的API两个版本有所差异,下面分别介绍一下。
4.1.0 获取商品价格
在4.1.0版本,内购商品和订阅商品获取商品价格使用统一的API,代码如下:
String replaceCurrencySymbol(String priceStr, String currencyCode, String currencySymbol) {
if (priceStr.startsWith("$")) {
if (currencySymbol != null) {
if (currencySymbol.equals(currencyCode)) {
//没有货币符号的情况,把货币码拼接到前面
priceStr = currencySymbol + priceStr;
} else {
if (!priceStr.startsWith(currencySymbol)) {
priceStr = priceStr.replace("$", currencySymbol);
}
}
}
}
return priceStr;
}
for (SkuDetails skuDetail : skuInfoList) {
String googleProductPrice = skuDetail.getPrice();
String googleCurrencyCode = skuDetail.getPriceCurrencyCode();
String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
}
5.0.0 获取商品价格
在5.0.0版本,内购商品和订阅商品获取商品价格使用不同的API, 代码如下:
String replaceCurrencySymbol(String priceStr, String currencyCode, String currencySymbol) {
if (priceStr.startsWith("$")) {
if (currencySymbol != null) {
if (currencySymbol.equals(currencyCode)) {
//没有货币符号的情况,把货币码拼接到前面
priceStr = currencySymbol + priceStr;
} else {
if (!priceStr.startsWith(currencySymbol)) {
priceStr = priceStr.replace("$", currencySymbol);
}
}
}
}
return priceStr;
}
for (ProductDetails productDetail : productDetails) {
if (BillingClient.ProductType.INAPP.equals(productDetail.getProductType()) && productDetail.getOneTimePurchaseOfferDetails() != null) {
String googleProductPrice = productDetail.getOneTimePurchaseOfferDetails().getFormattedPrice();
String googleCurrencyCode = productDetail.getOneTimePurchaseOfferDetails().getPriceCurrencyCode();
String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
} else if (BillingClient.ProductType.SUBS.equals(productDetail.getProductType()) && productDetail.getSubscriptionOfferDetails() != null) {
String googleProductPrice = productDetail.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
String googleCurrencyCode = productDetail.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode();
String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
}
}
启动购买
需要在主线程中调用launchBillingFlow
,启动购买的API在两个版本之间有一些不同,下面分别介绍一下。
4.1.0 启动购买
//将要购买商品的商品详情配置到参数中
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails("SkuDetails")
.setObfuscatedAccountId(availableGoods.getOrderNum())
.build();
//启动购买,返回BillingResult。
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
5.0.0 启动购买
ArrayList<BillingFlowParams.ProductDetailsParams> params = new ArrayList<>();
//将要购买商品的商品详情配置到参数中,两种类型的商品有所区别
if (BillingClient.ProductType.SUBS.equals(productDetailInfo.getProductType()) && productDetailInfo.getSubscriptionOfferDetails() != null) {
params.add(BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetailInfo)
.setOfferToken(productDetailInfo.getSubscriptionOfferDetails().get(0).getOfferToken())
.build());
} else {
params.add(BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetailInfo)
.build());
}
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(params)
.setObfuscatedAccountId(availableGoods.getOrderNum())
.build();
//启动购买,返回BillingResult。
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
核销订单
完成购买之后,还需要核销订单。如果三天之内没有核销订单的话, GooglePlay会取消交易,退款给用户。并且商品如果不核销则无法再次购买。
在调用谷歌的核销API之前,还可以通过服务器验证一下交易。
核销订单根据商品类型有不同的API,可重复购买的内购型商品使用consumeAsync
,不可重复购买的内购型商品和订阅型商品使用acknowledgePurchase
。
如果网络通畅,可以在初始化时配置的purchasesUpdatedListener
中收到回调并进行核销订单的处理,具体使用代码如下:
//核销回调
ConsumeResponseListener consumeResponseListener=new ConsumeResponseListener() {
@Override
public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) {
//核销完成后回调
}
};
//核销回调
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener=new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
//核销完成后回调
}
};
PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
switch (billingResult.getResponseCode()) {
case BillingClient.BillingResponseCode.OK:
//购买商品成功
for (Purchase purchase : purchases) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
//通过服务器验证订单,此处省略
//可重复购买的内购商品核销
consumePurchase(purchase.getPurchaseToken());
//不可重复购买的内购商品、订阅商品核销
acknowledgedPurchase(purchase.getPurchaseToken());
}
}
break;
case BillingClient.BillingResponseCode.USER_CANCELED:
//取消购买
break;
default:
//购买失败,具体异常码可以到BillingClient.BillingResponseCode中查看
break;
}
}
};
void consumePurchase(String purchaseToken) {
if (billingClient != null && billingClient.isReady()) {
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build();
billingClient.consumeAsync(consumeParams, consumeResponseListener);
}
}
void acknowledgedPurchase(String purchaseToken) {
if (billingClient != null && billingClient.isReady()) {
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
查询购买交易
为了避免在异常情况下(例如网络出现问题),无法通过purchasesUpdatedListener
核销订单,可以使用queryPurchasesAsync
来查询购买交易。
查询购买交易的API在两个版本之间有一些不同,下面分别介绍一下。
4.1.0 查询购买交易
PurchasesResponseListener purchasesResponseListener =new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
//list为购买交易的集合
}
};
//内购商品交易查询
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, purchasesResponseListener);
//订阅商品交易查询
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS, purchasesResponseListener);
5.0.0 查询购买交易
PurchasesResponseListener purchasesResponseListener =new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
//list为购买交易的集合
}
};
//内购商品交易查询
QueryPurchasesParams inAppPurchasesQuery = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchasesAsync(inAppPurchasesQurey, purchasesResponseListener);
//订阅商品交易查询
QueryPurchasesParams subscriptionsPurchasesQuery = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(subscriptionsPurchasesQurey, purchasesResponseListener);
5.0版本SDK兼容低版本GooglePlay
后续使用过程中,发现5.0版本支付SDK仍然包含4.1版本支付SDK的API,虽然调用时会提示已废弃,但仍然可用。并且,支付SDK中提供了一个方法isFeatureSupported
,用于判读设备是否支持新版本Product的API,示例代码如下:
// 在初始化Billing SDk之后调用
val result = billingClient.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS)
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
// 可以使用新版Product API
} else {
// 使用旧版SKU API
}
测试支付
到GooglePlay后台创建商品,需要注意的是要创建商品必须先发布一个带有Billing库的aab到GooglePlay(测试渠道即可)。
获取并配置好商品id之后,将测试用的aab发布到内部测试,并添加测试人员的邮箱。
通过连接分享给测试人员。
测试人员接受邀请后,就可以测试支付。
如果想要用测试卡支付,还需要在许可测试中添加测试人员的谷歌账号。