前一段时间由于项目需求,产品需要接入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() {
@Override
public void run() {
mBillingUpdatesListener.onBillingClientSetupFinished();
Log.d(TAG, "设置客户端成功,开始请求商品库存");
OnQueryPurchases();
}
});
}
/*开始连接Play*/
public void startServiceConnection(final Runnable executeOnSuccess){
billingClient.startConnection(new BillingClientStateListener() {
@Override
public 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();
}
@Override
public void onBillingServiceDisconnected() {
mIsServiceConnected = false;
}
});
}
/*请求商品库存*/
public void OnQueryPurchases() {
Runnable queryToExecute = new Runnable() {
@Override
public 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());
}
/*更新商品*/
@Override
public 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() {
@Override
public 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() {
@Override
public void run() {
// Consume the purchase async
billingClient.consumeAsync(consumeParams, onConsumeListener);
}
};
executeServiceRequest(consumeRequest);
}
/*查询内购商品详情*/
public void querySkuDetailsAsync(@SkuType final String itemType, final List<String> skuList,
final SkuDetailsResponseListener listener) {
Runnable queryRequest = new Runnable(){
@Override
public void run() {
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(itemType);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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();
}