完整流程Google Pay 接入
前一段时间由于项目需求,产品需要接入Google Pay SDK,然后....大家都懂的...各种搜索,出现的文章要么就是很久以前的,要么就是各种问题,经过一番"泥里打滚"后,还是默默的选择了官方的教程文档,结果好吧官方的demo也不是直接能用的,经过一番修改后,最终测试通过了,目前线上使用中...代码里都有详细的注释,大家有不懂的可以评论区留言,第一次写博客,若有不适请多海涵哦
上代码
首先是一个购买管理类BillingManager
package com.example.googleiap;import android.app.Activity;
import android.support.annotation.Nullable;
import android.util.Log;import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient.FeatureType;
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.Purchase.PurchasesResult;
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 java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;public class BillingManager implements PurchasesUpdatedListener{private static final String TAG = "BillingManager";/*购买key*/private static final String BASE_64_ENCODED_PUBLIC_KEY = "这里填写你的Google后台生成的一串Base64秘钥";/*未初始化标记*/public static final int BILLING_MANAGER_NOT_INITIALIZED = -1;/*客户端*/private BillingClient billingClient;/*活动*/private final Activity mActivity;/*监听*/private final BillingUpdatesListener mBillingUpdatesListener;/*是否连接成功*/private boolean mIsServiceConnected;/*客户端当前状态*/private @BillingResponseCode int curBillingClientResponseCode = BillingResponseCode.SERVICE_DISCONNECTED;/*商品列表*/private final List<Purchase> PurchaseList = new ArrayList<>();/*消耗令牌*/private Set<String> mTokensToBeConsumed;/*监听接口*/public interface BillingUpdatesListener{void onBillingClientSetupFinished();void onConsumeFinished(String token, @BillingResponseCode int result);void onPurchasesUpdated(List<Purchase> purchases);void onFailedHandle(@BillingResponseCode int result);}public BillingManager(Activity activity,final BillingUpdatesListener updatesListener){Log.d(TAG, "创建Billing客户端");mActivity = activity;mBillingUpdatesListener = updatesListener;billingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build();Log.d(TAG, "开始设置信息");startServiceConnection(new Runnable() {@Overridepublic void run() {mBillingUpdatesListener.onBillingClientSetupFinished();Log.d(TAG, "设置客户端成功,开始请求商品库存");OnQueryPurchases();}});}/*开始连接Play*/public void startServiceConnection(final Runnable executeOnSuccess){billingClient.startConnection(new BillingClientStateListener() {@Overridepublic void onBillingSetupFinished(BillingResult billingResult) {Log.d(TAG, "Setup finished. Response code: " + billingResult);if(billingResult.getResponseCode() == BillingResponseCode.OK){mIsServiceConnected = true;if(executeOnSuccess != null){executeOnSuccess.run();}}curBillingClientResponseCode = billingResult.getResponseCode();}@Overridepublic void onBillingServiceDisconnected() {mIsServiceConnected = false;}});}/*请求商品库存*/public void OnQueryPurchases() {Runnable queryToExecute = new Runnable() {@Overridepublic void run() {//系统当前时间long time = System.currentTimeMillis();//请求内购商品PurchasesResult purchasesResult = billingClient.queryPurchases(SkuType.INAPP);Log.i(TAG, "请求请内购商品花费时间:" + (System.currentTimeMillis() - time) + "ms");//支持订阅if(areSubscriptionsSupported()){PurchasesResult subscriptionResult = billingClient.queryPurchases(SkuType.SUBS);Log.i(TAG, "请求订阅商品后花费的时间: "+ (System.currentTimeMillis() - time) + "ms");if(subscriptionResult.getResponseCode() == BillingResponseCode.OK){Log.i(TAG, "请求订阅消息返回 Code: "+ subscriptionResult.getResponseCode()+ " res: " + subscriptionResult.getPurchasesList().size());purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList());}else {Log.e(TAG, "获取订阅商品失败请见Code");}}else if (purchasesResult.getResponseCode() == BillingResponseCode.OK){Log.i(TAG, "跳过请求订阅商品,因为设备不支持");}else{Log.w(TAG, "请求商品失败返回: "+ purchasesResult.getResponseCode());}onQueryPurchasesFinished(purchasesResult);}};executeServiceRequest(queryToExecute);}/*是否支持订阅*/public boolean areSubscriptionsSupported(){int responseCode = billingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS).getResponseCode();if(responseCode != BillingResponseCode.OK){Log.w(TAG, "areSubscriptionsSupported() got an error response: " + responseCode);}return responseCode == BillingResponseCode.OK;}/*请求商品信息完成*/private void onQueryPurchasesFinished(PurchasesResult result){if(billingClient == null || result.getResponseCode() != BillingResponseCode.OK){Log.w(TAG, "billingClient is null or result code (" + result.getResponseCode()+ ") was bad - quitting");return;}Log.d(TAG, "请求商品信息完成");PurchaseList.clear();onPurchasesUpdated(result.getBillingResult(),result.getPurchasesList());}/*更新商品*/@Overridepublic void onPurchasesUpdated(BillingResult billingResult,List<Purchase> purchases){if(billingResult.getResponseCode() == BillingResponseCode.OK){for (Purchase purchase : purchases){HandlePurchase(purchase);}mBillingUpdatesListener.onPurchasesUpdated(PurchaseList);}else{if(billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED){Log.i(TAG, "onPurchasesUpdated() - 用户取消购买当前商品");}else{Log.w(TAG, "onPurchasesUpdated() got unknown resultCode: " + billingResult.getResponseCode());}mBillingUpdatesListener.onFailedHandle(billingResult.getResponseCode());}}/*商品处理*/private void HandlePurchase(Purchase purchase){//验证签名数据Log.i(TAG,"getSignature => "+ purchase.getSignature());if(!VerifyValidSignature(purchase.getOriginalJson(),purchase.getSignature())){Log.i(TAG, "Got a purchase: " + purchase + "; but signature is bad. Skipping...");return;}Log.d(TAG, "Got a verified purchase: " + purchase);PurchaseList.add(purchase);}/*验证签名*/private boolean VerifyValidSignature(String signedData,String signature){try{return Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY,signedData,signature);}catch (IOException e){Log.e(TAG, "Got an exception trying to validate a purchase: " + e);return false;}}/*执行服务请求*/private void executeServiceRequest(Runnable runnable) {if(mIsServiceConnected){runnable.run();}else{startServiceConnection(runnable);}}public void consumeAsync(final String purchaseToken) {if (mTokensToBeConsumed == null) {mTokensToBeConsumed = new HashSet<>();} else if (mTokensToBeConsumed.contains(purchaseToken)) {Log.i(TAG, "Token was already scheduled to be consumed - skipping...");return;}mTokensToBeConsumed.add(purchaseToken);//消耗监听final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() {@Overridepublic void onConsumeResponse(BillingResult responseCode, String purchaseToken) {// If billing service was disconnected, we try to reconnect 1 time// (feel free to introduce your retry policy here).mBillingUpdatesListener.onConsumeFinished(purchaseToken, responseCode.getResponseCode());}};final ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build();Runnable consumeRequest = new Runnable() {@Overridepublic void run() {// Consume the purchase asyncbillingClient.consumeAsync(consumeParams, onConsumeListener);}};executeServiceRequest(consumeRequest);}/*查询内购商品详情*/public void querySkuDetailsAsync(@SkuType final String itemType, final List<String> skuList,final SkuDetailsResponseListener listener) {Runnable queryRequest = new Runnable(){@Overridepublic void run() {SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();params.setSkusList(skuList).setType(itemType);billingClient.querySkuDetailsAsync(params.build(),new SkuDetailsResponseListener() {@Overridepublic void onSkuDetailsResponse(BillingResult billingResult,List<SkuDetails> skuDetailsList) {listener.onSkuDetailsResponse(billingResult, skuDetailsList);}});}};executeServiceRequest(queryRequest);}// /*开始一个购买流程*/
// public void initiatePurchaseFlow(final SkuDetails skuDetails, final @SkuType String billingType) {
// initiatePurchaseFlow(skuDetails);
// }/*启动购买,订购流程*/public void initiatePurchaseFlow(final SkuDetails skuDetails) {Runnable purchaseFlowRequest = new Runnable() {@Overridepublic void run() {
// Log.d(TAG, "Launching in-app purchase flow. Replace old SKU? " + (oldSkus != null));BillingFlowParams purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build();billingClient.launchBillingFlow(mActivity, purchaseParams);}};executeServiceRequest(purchaseFlowRequest);}public void acknowledgePurchase(AcknowledgePurchaseParams acknowledgePurchaseParams, AcknowledgePurchaseResponseListener Listener){billingClient.acknowledgePurchase(acknowledgePurchaseParams,Listener);}/* 释放连接*/public void destroy(){Log.d(TAG, "Destroying the manager.");if (billingClient != null && billingClient.isReady()) {billingClient.endConnection();billingClient = null;}}}
然后是一个商品验证类
package com.example.googleiap;import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;import com.android.billingclient.util.BillingHelper;import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;public class Security {private static final String TAG = "GoogleIap/Security";private static final String KEY_FACTORY_ALGORITHM = "RSA";private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";public static boolean verifyPurchase(String base64PublicKey, String signedData,String signature) throws IOException {if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)|| TextUtils.isEmpty(signature)) {Log.w(TAG,"购买验证失败,数据丢失");return false;}PublicKey key = generatePublicKey(base64PublicKey);return verify(key, signedData, signature);}public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException {try {byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));} catch (NoSuchAlgorithmException e) {// "RSA" is guaranteed to be available.throw new RuntimeException(e);} catch (InvalidKeySpecException e) {String msg = "Invalid key specification: " + e;BillingHelper.logWarn(TAG, msg);throw new IOException(msg);}}public static boolean verify(PublicKey publicKey, String signedData, String signature) {byte[] signatureBytes;try {signatureBytes = Base64.decode(signature, Base64.DEFAULT);} catch (IllegalArgumentException e) {BillingHelper.logWarn(TAG, "Base64 decoding failed.");return false;}try {Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM);signatureAlgorithm.initVerify(publicKey);signatureAlgorithm.update(signedData.getBytes());if (!signatureAlgorithm.verify(signatureBytes)) {BillingHelper.logWarn(TAG, "Signature verification failed.");return false;}return true;} catch (NoSuchAlgorithmException e) {// "RSA" is guaranteed to be available.throw new RuntimeException(e);} catch (InvalidKeyException e) {BillingHelper.logWarn(TAG, "Invalid key specification.");} catch (SignatureException e) {BillingHelper.logWarn(TAG, "Signature exception.");}return false;}}
我是把BillingManager和Security 单独作为库文件是因为方便以后的项目移植
然后是使用方法了
本类不处理任何商品逻辑,只作为中转站,下面是部分使用的逻辑
在你的android工程中创建类继承至
extends UnityPlayerActivity implements BillingManager.BillingUpdatesListener
用到的成员属性
private static Dictionary<String,SkuDetails> ProductList = new Hashtable<String,SkuDetails>();
/*商品token*/
private static Dictionary<String,Purchase> m_PurchaseListByToken = new Hashtable<>();
/*商品*/
private static Dictionary<String,Purchase> m_PurchaseListBySku = new Hashtable<>();
/*内购是否初始化完成*/
private boolean bSetupFinis;
/*商品是否初始化完成*/
private boolean bInitProduct;
private final String Buy = "Buy";
private final String Consume = "Consume";
private final String Success = "Success";
/*内购客户端*/
private BillingManager m_BillingManager;
然后在你项目需要的地方去初始化BillingManager,我的项目是游戏loading加载完后初始化
/*初始化*/
public void InitBilling(String nil){Log.w(TAG,"InitBilling");m_BillingManager = new BillingManager(this,this);}/*客户端设置成功回调*/@Override public void onBillingClientSetupFinished(){bSetupFinis = true;Log.i(TAG, "onBillingClientSetupFinished");BeginQuestProuect();}
然后就是初始化商品列表(在你觉得什么时候可以调用的地方调用就是了),有人会想我不是在谷歌后台配置了商品列表吗?怎么还要初始化商品呢,这里是因为要获取一些购买时的所需要的验证数据,所以得请求一次,把你本地的商品配置的id用符号格式化,然后把返回的商品信息存下来 ,ProductList.put(skuDetail.getSku(),skuDetail);
public void InitProduceList(String productid){String[] sArray = productid.split("#");for(String sku : sArray){ProductKeys.add(sku);}bInitProduct = true;BeginQuestProuect();
}/*准备请求商品信息*/
private void BeginQuestProuect(){if(!bInitProduct || !bSetupFinis){return;}/*我这里是为了图方便都注册一次*/addProduct(ProductKeys,SkuType.INAPP);addProduct(ProductKeys,SkuType.SUBS);
}/*添加商品*/public void addProduct(List<String> skusList, final @SkuType String billingType){m_BillingManager.querySkuDetailsAsync(billingType, skusList, new SkuDetailsResponseListener() {@Overridepublic void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {if (billingResult.getResponseCode() != BillingResponseCode.OK) {Log.w(TAG, "Unsuccessful query for type: " + billingType+ ". Error code: " + billingResult.getResponseCode());}else if(skuDetailsList != null && skuDetailsList.size() > 0){for(SkuDetails skuDetail : skuDetailsList){Log.i(TAG, "Adding sku: " + skuDetail);/*把返回的商品信息存下来*/ProductList.put(skuDetail.getSku(),skuDetail);}}}});}
商品更新的回调,第一次初始化内购的时候也会返回一些商品信息,这里我们保留已经购买的和激活(这个意思就是说待确认的商品)的商品,购买成功也走的这里,那怎么区分是购买还是初始化触发的呢?因为购买商品肯定是一次一次请求的,如果已经在购买一类商品肯定是需要等待这次购买结束才能发起下一次购买的(所以你判断有没有购买请求就知道啦)
/*商品更新回调*/@Override public void onPurchasesUpdated(List<Purchase> purchases){UnitySendMessage("State.0");String parame;for (Purchase purchase : purchases){if (purchase.getPurchaseState() == PurchaseState.PURCHASED || purchase.isAcknowledged()) {m_PurchaseListByToken.put(purchase.getPurchaseToken(),purchase);m_PurchaseListBySku.put(purchase.getSku(),purchase);parame = String.format("%s.%s.0.%s",Buy,purchase.getSku(),purchase.getPurchaseTime() / 1000);UnitySendMessage(parame);}}UnitySendMessage("State.1");Log.i(TAG, "onPurchasesUpdated");}/*失败处理*/public void onFailedHandle(@BillingResponseCode int result){String code = String.format("%s.-1.%s.0",Buy,result);Log.i(TAG, "onFailedHandle code = " + code);UnitySendMessage(code);}
然后是购买商品,这里就用到了之前初始化所需要的商品数据了
/*购买商品*/public void BuyProduct(String product){Log.d(TAG,"BuyProduct:" + product);SkuDetails skuDetails= ProductList.get(product);if(skuDetails != null){m_BillingManager.initiatePurchaseFlow(skuDetails);}else{Log.w(TAG, "not found product in ProductList ,product=> " + product);}}
然后是消耗商品了,这里请注意!!!google后台是不区分消耗和非消耗商品,这里需要你自己来管理,通过配置判断
/*消耗商品*/public void consumeAsync(String product,String type){
// type = "2";Log.i(TAG,"consumeAsync product = " + product + ",type = " + type);Purchase purchase = m_PurchaseListBySku.get(product);if(purchase != null){m_PurchaseListBySku.remove(product);if(type.equals("1")){ //消耗品m_BillingManager.consumeAsync(purchase.getPurchaseToken());}else if (type.equals("0")){if(!purchase.isAcknowledged()){Log.i(TAG,"前往确认商品");AcknowledgePurchaseParams acknowledgePurchaseParams =AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();m_BillingManager.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {@Overridepublic void onAcknowledgePurchaseResponse(BillingResult billingResult) {Log.i(TAG,"前往确认商品返回 code = " +billingResult.getResponseCode() );String parame = String.format("%s.-1.%s.-1",Consume,billingResult.getResponseCode());UnitySendMessage(parame);}});}else{String parame = String.format("%s.-1.0.-1",Consume);UnitySendMessage(parame);}}}}
商品消耗完成
/*商品消耗完成回调*/@Override public void onConsumeFinished(String token, @BillingResponseCode int result){Purchase purchases = m_PurchaseListByToken.get(token);Log.i(TAG, "onConsumeFinished (purchases)=>" + purchases);if(purchases != null){String code = String.format("%s.%s.%s.%s",Consume,purchases.getSku(),result,purchases.getPurchaseTime());UnitySendMessage(code);m_PurchaseListByToken.remove(purchases.getPurchaseToken());}}
然后是后台切换的时候也需要调用一次商品更新,因为有可能玩家是在谷歌商店购买的游戏商品
@Override protected void onResume(){super.onResume();if(m_BillingManager != null){m_BillingManager.OnQueryPurchases();}}
补充:项目的build.gradle设置
dependencies{implementation 'com.android.billingclient:billing:2.0.3'
}
断开连接
@Override protected void onDestroy (){if(m_BillingManager != null){m_BillingManager.destroy();}super.onDestroy();}
完整流程Google Pay 接入相关推荐
- Google Pay接入
1.由于业务需求,准备接入Google Pay,一开始本人接到这个需求的时候,就开始到Google Pay官网以及Google.百度上搜索如何接入Google Pay,也是有发现一些文章.在我处理了一 ...
- Android 接入google pay
文章目录 google pay google play Billing 支持的一次性产品 商品购买流程 google pay 实现流程 1. 添加依赖 2. 连接到Google Play 3. 查询商 ...
- Google Pay支付遇到的问题
Google Pay 我们发现要显示的任何SKU,检查您的互联网连接并确保您的Google Developer Console设置正确. 应用未通过审核或应用内商品ID传入有误,要先测 ...
- Flutter集成Google、Facebook等第三方登陆完整流程
前言 国内的登陆一般可以通过极光.友盟等这些第三方平台提供的sdk,实现一键接入.国外的就比较杂了,比如常用的 GitHub.Twitter.apple.Microsoft等, Google 提供了 ...
- APP接入支付宝支付完整流程及踩坑记录(含服务端)
本篇主要讲解APP接入支付宝支付完整流程,包含服务端,内容稍长 要接入支付宝支付,需要将APP在支付宝平台创建应用,提交审核,并进行商户签约以获得支付能力 详细参阅官方文档https://docs.o ...
- Vue - 超详细网站接入腾讯地图的完整流程,提供地图显示、IP 属地定位、地理位置名称、获取经纬度等超多功能示例(可一键复制并运行的功能源代码,详细的注释及常见问题汇总)Nuxt.js 也能用!
前言 如果您需要 uniapp 教程,请访问:uniapp - H5 网页接入腾讯地图. 本文站在小白的角度,实现了 Vue.js / Nuxt.js 网站,集成腾讯地图的详细流程及使用方法教程,提供 ...
- Google Pay 谷歌支付(gateway = stripe)
Google pay 国际版开发 一.官网地址(科学上网) 官方对接文档 https://developers.google.com/pay/api/android/overview Stripe对接 ...
- 服务端验证Google Pay订单的两种方式
Google Pay主要支付流程: 1.手机端向服务端发起支付,生成预订单,给手机端返回生成的订单号 2.手机端向Google发起支付(传入本地服务器生成的订单号) 3.Google服务器将支付结果返 ...
- 关于Google Pay JAVA后端处理
Google Pay JAVA后端处理 前言:最近接了个需求,关于谷歌支付的处理流程.觉得有必要记录下来,在网上也找了很多资料,不 全.怎么个不全法呢? *第一:很多人用的方法就是使用谷歌的publi ...
最新文章
- linux命令下怎么保存python_Linux 环境下安装 Python3 的操作方法
- 使用 PowerShell 创建 Linux 虚拟机
- 理解Python的迭代器
- 老板怒了,“我们赚钱你们花钱,还总出毛病!”
- python-nmap使用及案例
- 一些经典的前端文章合集地址
- 年终总结系列2:人人都在讲的全面风险管理,真的做到了吗?
- css滑动星星评分,纯css3滑动星星打分动画特效
- Fragment与FragmentActivity通信封装
- [转载] Java异常处理中Try-Catch-Finally中常见的笔试题
- superset报错
- 标定_基于目标的激光雷达与相机外参标定方法汇总
- python怎么让py里面逐行运行_Python读写文件详解,看完这篇即可完全理解「收藏」...
- 密码编码学与网络安全
- java 求任意输入半径,圆的周长和面积。
- MinGW与Clion下载安装及使用详解
- 面试心得与总结---BAT、网易、蘑菇街
- 基于Python的视频中的人脸识别系统设计与实现
- 沁恒蓝牙芯片CH58X蓝牙从机的使用
- Qt音视频开发7-ffmpeg音频播放