海外Google Play-v4.0結(jié)算庫流程

前言

最近看群聊宰闰,一位兄弟去面試安卓SDK崗位辛块,面試時候被問到了google play結(jié)算流程浊猾。這位兄弟平時主要是負(fù)責(zé)國內(nèi)的SDK渠道,海外SDK基本是沒有了解阶冈。

結(jié)果面試過程一臉尷尬闷尿,面完后在群里也分享了一下面試過程,正好最近公司要更新一下google play 結(jié)算庫 4.0女坑,順便我做個分享填具,希望群里哪位兄弟能看見 。

了解一下最近幾個版本結(jié)算庫的變化

Google Play 結(jié)算庫 4.0 版 (2021-05-18)

當(dāng)前最新版本,變更內(nèi)容

  • 添加了 BillingClient.queryPurchasesAsync() 以替換 BillingClient.queryPurchases()匆骗,我們將在未來的版本中移除后者劳景。

  • 添加了新的訂閱替換模式 IMMEDIATE_AND_CHARGE_FULL_PRICE。

  • 添加了 BillingClient.getConnectionState() 方法碉就,用于檢索 Play 結(jié)算庫的連接狀態(tài)盟广。

  • 更新了 Javadoc 和實現(xiàn),用于指明可在哪個線程上調(diào)用方法以及發(fā)布哪些線程結(jié)果瓮钥。

  • 添加了 BillingFlowParams.Builder.setSubscriptionUpdateParams() 作為發(fā)起訂閱更新的新方式筋量,用于替換已移除的

    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 結(jié)算庫 3.0.3 版 (2021-03-12)

  • 修復(fù)了在調(diào)用 endConnection() 時發(fā)生內(nèi)存泄漏的問題跃惫。

  • 修復(fù)了利用單個任務(wù)啟動模式的應(yīng)用在使用 Google Play 結(jié)算庫時出現(xiàn)的問題叮叹。當(dāng)應(yīng)用從 Android 啟動器恢復(fù)運行且結(jié)算對話框在暫停之前可見時,將觸發(fā) onPurchasesUpdated() 回調(diào)爆存。

  • Unity 問題修復(fù)
    更新到了 Java 3.0.3蛉顽,解決了內(nèi)存泄漏問題,并解決了當(dāng)應(yīng)用從 Android 啟動器恢復(fù)運行且結(jié)算對話框在暫停之前可見時出現(xiàn)的無法購買的問題先较。


Google Play 結(jié)算庫 3.0.2 版 (2020-11-24)

問題修復(fù)

  • 修復(fù)了 Kotlin 擴展程序中的一個錯誤:協(xié)程失敗并顯示錯誤“Already resumed”携冤。

  • 修復(fù)了將 Kotlin 擴展程序與 kotlinx.coroutines 庫版本 1.4 及更高版本一起使用時未解析的引用。


Google Play 結(jié)算庫 3.0.1 版 (2020-09-30)

問題修復(fù)

  • 修復(fù)了以下錯誤:如果在結(jié)算過程中終止后恢復(fù)應(yīng)用闲勺,系統(tǒng)可能不會使用購買結(jié)果調(diào)用 PurchasesUpdatedListener曾棕。

Google Play 結(jié)算庫 4.0版需要關(guān)心的事情

根據(jù)需求,我司游戲采用了一次性商品模式菜循,訂閱模式相關(guān)不作說明翘地,本文著重講解一次性商品模式晴及,往下看我會解釋訂閱和一次性商品兩種模式的概念

一次性模式建議用BillingClient.queryPurchasesAsync(),BillingClient.queryPurchases()后續(xù)會被刪除掉臼婆。此方法官方描述:異步操作,返回活動訂閱和非消費的一次性購買。

這個方法使用谷歌Play Store應(yīng)用的緩存及皂,而不需要發(fā)起網(wǎng)絡(luò)請求。注:建議購服務(wù)端通過調(diào)用下面文檔接口做安全驗證

BillingClient.getConnectionState()方法挑庶,用于檢索 Play 結(jié)算庫的連接狀態(tài)亚侠。這個方法我查一下官方Demo并沒有使用,Google搜索也并沒有使用對應(yīng)blog渴杆,暫不建議使用寥枝。

新增兩個方法

Purchase.getQuantity() :表示應(yīng)用內(nèi)付費購買。返回購買的商品數(shù)量 磁奖,訂閱方式 固定返回1囊拜、一次性商品方式大于1

PurchaseHistoryRecord.getQuantity():應(yīng)用內(nèi)付費購買歷史記錄,返回購買的商品數(shù)量 比搭,訂閱方式 固定返回1 冠跷、一次性商品方式大于1

Purchase.getSku()被Purchase.getSkus() 代替了,getSku()方法被刪除掉 身诺。

還有一個重點 endConnection() 升級到4.x修復(fù)了內(nèi)存泄漏的問題蜜托。

BillingClient重要說明

BillingClient用于庫和用戶應(yīng)用程序代碼之間通信的主界面。為應(yīng)用內(nèi)計費提供了便利的方法霉赡。應(yīng)用程序創(chuàng)建該類的一個實例橄务,并使用它來處理應(yīng)用程序內(nèi)的計費操作。它為許多常見的應(yīng)用內(nèi)計費操作提供了同步(阻塞)和異步(非阻塞)方法穴亏。

強烈建議一次只實例化一個BillingClient實例蜂挪,以避免多個PurchasesUpdatedListener。onPurchasesUpdated(BillingResult, List) 回調(diào)單個事件嗓化。

所有帶AnyThread注釋的方法都可以從任何線程調(diào)用棠涮,所有異步回調(diào)都將在同一個線程上返回。用UiThread注釋的方法應(yīng)該從Ui(主)線程調(diào)用刺覆,所有的異步回調(diào)也將在Ui(主)線程上返回严肪。

AnyThread方法.png

實例化后,必須執(zhí)行設(shè)置才能開始使用該對象谦屑。要執(zhí)行設(shè)置驳糯,調(diào)用startConnection(BillingClientStateListener)方法并提供一個監(jiān)聽器;當(dāng)完成時,該監(jiān)聽器將得到通知氢橙,在此之后(而不是之前)结窘,你可以開始調(diào)用其他方法。

當(dāng)你使用完這個對象時充蓝,不要忘記調(diào)用endConnection()以確保正確的清理該對象隧枫。該對象持有與應(yīng)用內(nèi)計費服務(wù)和管理器的綁定喉磁,用于處理廣播事件,除非你能正確地處理它官脓,否則廣播事件將泄漏协怒。

如果你在Activity.onCreate(Bundle)方法中創(chuàng)建了對象,那么建議將endConnection()方法放到Activity.onDestroy()方法中卑笨。清理后孕暇,不能再為連接重用它。

如何處理BillingClient.onBillingServiceDisconnected()

關(guān)于是否在Google服務(wù)斷開后重連的問題赤兴,建議通過回調(diào)讓游戲做提示妖滔,不做重連邏輯。在4.0官方demo中注釋寫到嘗試再次連接服務(wù) 桶良,其實是什么都沒有做的座舍。

在查閱一番資料在v3.0結(jié)算庫斷開后重新連接,可能會出現(xiàn)異常閃退的問題陨帆。4.0中每次購買前可以做以下邏輯判斷曲秉。

  • BillingClient是否初始化成功

  • BillingClient 是否 isReady()

  • SKU詳細(xì)信息內(nèi)容不能為空

 //Google Play應(yīng)用內(nèi)結(jié)算v3,部分三星手機上出現(xiàn)異常疲牵,
 //崩潰日志結(jié)果與調(diào)用的BillingClient.onBillingServiceDisconnected()有關(guān)承二。
 
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 結(jié)算庫一次性交易的生命周期

  1. 向用戶展示他們可以購買什么。

  2. 啟動購買流程纲爸,以便用戶接受購買交易亥鸠。

  3. 可以在你的服務(wù)器上驗證購買交易。

  4. 向用戶提供內(nèi)容识啦,并確認(rèn)內(nèi)容已傳送給用戶读虏。還可以選擇性地將商品標(biāo)記為已消費,以便用戶可以再次購買商品袁滥。

Google play 結(jié)算庫集成步驟

先給大家概念的解釋一下什么是一次性商品和 訂閱類型商品

BillingClient.SkuType.INAPP : 針對一次性商品,通過用戶的付款方式重復(fù)持續(xù)性質(zhì)的購買灾螃,也稱為內(nèi)購

  • 一次性商品: 可多次購買题翻,例如:6美元 10K金幣

  • 非消耗類型商品: 只能購買一次就能永久使用的商品, 例如:關(guān)卡包升到對應(yīng)等級只能購買一次

BillingClient.SkuType.SUBS : 訂閱是一種讓用戶定期使用內(nèi)容的商品腰鬼。訂閱期結(jié)束后嵌赠,訂閱會自動續(xù)訂,并且會通過用戶的付款方式向用戶另行收取費用熄赡。訂閱會無限期續(xù)訂姜挺,直到被取消。訂閱的示例包括在線雜志瀏覽和音樂在線播放服務(wù)等等彼硫。

  • 例如購買某雷會員炊豪,周期性購買凌箕,你可以按月支付、也可以按季度词渤、和年來支付牵舱。


1 . 初始化BillingClient

BillingClient 是 Google Play 結(jié)算庫與應(yīng)用的其余部分之間進行通信的主接口

private BillingClient billingClient = BillingClient.newBuilder(activity)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .build();

2 . 與Google Play 建立連接

連接到Google pay的過程是異步的,所以是需要使用BillingClientStateListener監(jiān)聽缺虐,確保能夠成功的連接到Google Play

注意:請確保在執(zhí)行任何方法時都與 BillingClient 保持連接芜壁。

billingClient.startConnection(new BillingClientStateListener() {

     //異步連接,接收結(jié)果回調(diào)高氮,連接成功了進行下一步邏輯操作
    @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() {
       //嘗試在下次請求時重啟連接
        //谷歌通過調(diào)用startConnection()方法進行連接    
    }
});

3 . 展示可購買的商品

前置條件完成,已與Google play建立連接后剪芍,可以查詢可售商品塞淹,需要調(diào)用異步詳細(xì)信息接口querySkuDetailsAsync()。querySkuDetailsAsync()會返回本地化的商品信息

onSkuDetailsResponse回調(diào)會將查詢到的商品信息存儲在列表字段SkuDetails對象里面紊浩,SkuDetails對象可以展示商品相關(guān)信息例如價格窖铡、商品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) {
                    // 去處理結(jié)果
        }
    });

4 . 啟動購買流程(拉起支付,可見支付UI)

先調(diào)用querySkuDetailsAsync()獲取"skuDetails"的值, 創(chuàng)建BillingFlowParams對象坊谁。通過billingClient.launchBillingFlow拉起Google 支付UI界面费彼,responseCode等于0 即表示打開UI成功 ,要對其他的狀態(tài)進行邏輯處理。

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build();

int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();

//responseCode狀態(tài)進行邏輯判斷

5. 處理購買結(jié)果

在用戶退出 Google Play 購買界面時 (點擊 "購買" 按鈕完成購買口芍,或者點擊 "返回" 按鈕取消購買)箍铲,onPurchaseUpdated() 回調(diào)會將購買流程的結(jié)果發(fā)送回你的應(yīng)用。

然后鬓椭,根據(jù) BillingResult.responseCode 即可確定用戶是否成功購買產(chǎn)品颠猴。如果 responseCode == OK,則表示購買已成功完成小染。同時也要對其他的狀態(tài)進行邏輯處理翘瓮。

onPurchaseUpdated() 會傳回一個 Purchase 對象列表,其中包括用戶通過應(yīng)用進行的所有購買裤翩。每個 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 . 驗證和確認(rèn)購買

購買成功后,完成購買流程筐带。如果應(yīng)用未在 72 小時內(nèi)確認(rèn)購買今穿,則用戶會自動收到退款,并且 Google Play 會撤消該購買交易伦籍。這一步建議拿PurchaseToken等信息 去服務(wù)器做一下校驗蓝晒,防止信息被篡改腮出。

購買進行驗證之后,還需要對其進行確認(rèn)拔创。consumeAsync()是處理消耗性商品利诺,用來標(biāo)記為 "已消耗 (consumed)",使得用戶可以再次購買剩燥。

//消耗性商品 處理調(diào)用

void handlePurchase(Purchase purchase) {

  //從BillingClient#queryPurchasesAsync或你的PurchasesUpdatedListener檢索購買慢逾。
    Purchase purchase = ...;

//驗證購買。確保此purchaseToken的授權(quán)尚未被授予灭红。授予用戶權(quán)限侣滩。


    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 結(jié)算接入前置需要

提供一個空包(新建一個空白可運行工程即可)占位置变擒,然后提供給運營(正常一點的公司都應(yīng)該是這個流程)君珠,其中重點是applicationId 和.keystore文件這兩個文件,提交的versionName建議從1.0.0開始每次提版本+1即可娇斑。

applicationId是不可隨意更改策添,它與參數(shù) 配置等信息綁定。簽名文件(keystore)是否正確取決于你的支付是否能被正常拉起毫缆。拿到程序給的包體后唯竹,運營把包體提交谷歌后臺用于申請參數(shù)。

參數(shù)申請完成后運營會在Google后臺結(jié)算頁面配置商品id 等信息(不細(xì)說了苦丁,多了我也不知道浸颓,沒配置過)一定要配置、一定要配置旺拉、不配置的話支付拉不起产上。

上述的前置條件弄好后,先準(zhǔn)備一個穩(wěn)定科學(xué)上網(wǎng)的工具蛾狗,這個很重要這個決定是否連接上谷歌服務(wù)晋涣。測試機最好是用海外的手機例如goole pixel3,千萬別用華為手機沉桌,谷歌服務(wù)連接在華為手機上是被默認(rèn)閹割禁止掉的谢鹊。

申請一個谷歌賬號,然后讓運營把你的谷歌賬號添加為開發(fā)者蒲牧。因為我添加了開發(fā)者可以不需要真實支付,所以 谷歌賬號并沒有綁定海外銀行卡 赌莺。程序這塊需要運營提供一個google-services.json文件用于支付冰抢。

Google play 接入前需要的代碼環(huán)境配置

//最外層的build.gradle  需要加上Google 服務(wù)插件

apply from:"config.gradle"
buildscript {
    repositories {  //配置遠(yuǎn)程倉庫
        google()
        jcenter()
    }
    dependencies {  //配置構(gòu)建工具

        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()
    }
}
/**
 * 運行g(shù)radle clean時,執(zhí)行此處定義的task任務(wù)艘狭。
 */
task clean(type: Delete) {
    delete rootProject.buildDir
}

  //添加谷歌服務(wù)依賴
 apply plugin: 'com.google.gms.google-services'  // Google Services plugin

  //向 app/build.gradle 文件中添加依賴關(guān)系
 implementation 'com.android.billingclient:billing:4.0.0'

google-services.json文件存放的位置

google-services文件.png

Google Play結(jié)算代碼部分

說明:代碼中xxxx_coins_1.99為我司游戲挎扰,真實的谷歌后臺配置其中一個計費點翠订,為了避免不必要的麻煩用xxx代替,如果需要看demo支付結(jié)算代碼流程遵倦,需要更換成使用者真實配置的商品id尽超,真實開發(fā)中 計費點不可能為一個。下面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);
            }
        });
    }

    /**
     * 初始化創(chuàng)建BillingClient對象
     * isReady() :檢查客戶端當(dāng)前是否連接到服務(wù)似谁,以便對其他方法的請求將成功。
     */
     private  void  billingClientiCreate(){
         LogUtils.i(TAG, "BillingClienticreate");

        //在 onCreate() 中創(chuàng)建一個新的 BillingClient掠哥。由于 BillingClient 只能使用一次巩踏,因此我們需要在 onDestroy() 中結(jié)束之前與 Google Play 商店的連接后創(chuàng)建一個新實例

         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() {  //計費服務(wù)已斷開連接
                     LogUtils.i(TAG, "onBillingServiceDisconnected");
                     Toast.makeText(MainActivity.this,"計費服務(wù)已斷開連接,請檢查一下網(wǎng)絡(luò)是否有誤",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) { // //計費客戶端已準(zhǔn)備就緒续搀。您可以在此處查詢購買情況
                         querySkuDetails();
                         queryPurchases();
                     }
                 }
             });
         }
     }

    /**
     * 查詢已經(jīng)購買過但是沒有被消耗的商品塞琼,可能網(wǎng)絡(luò)不穩(wěn)定或者中斷導(dǎo)致的未被消耗
     * 如果購買成功沒消耗,就去消耗禁舷,消耗完成視為完整的流程彪杉。
     */
    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 接收結(jié)果,來顯示 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為空派近,應(yīng)該檢查你請求的 SKU 是否在 Google Play Console 中正確發(fā)布
                        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 只能使用一次构哺。
            // 在調(diào)用 endConnection() 之后,我們必須創(chuàng)建一個新的 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 游戲生成的訂單號曙强, 等服務(wù)器需要的信息,去游戲服務(wù)器去驗證途茫,
                    //游戲服務(wù)器再去google服務(wù)器驗證碟嘴,驗證成功后,通知游戲服務(wù)器囊卜,游戲服務(wù)器通知客戶端下發(fā)道具娜扇。
                    LogUtils.i(TAG, "onConsumeResponse 商品購買成功,下發(fā)道具======" +purchaseToken);
                }
            }
        };

        billingClient.consumeAsync(consumeParams, listener);  //去消耗道具
    }

}

Purchase對象屬性

Purchase屬性.png

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"
}

BillingResponseCode

BillingResponseCode.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雀瓢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子玉掸,更是在濱河造成了極大的恐慌刃麸,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件司浪,死亡現(xiàn)場離奇詭異泊业,居然都是意外死亡把沼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門吁伺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饮睬,“玉大人,你說我怎么就攤上這事篮奄±Τ睿” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵宦搬,是天一觀的道長牙瓢。 經(jīng)常有香客問我,道長间校,這世上最難降的妖魔是什么矾克? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮憔足,結(jié)果婚禮上胁附,老公的妹妹穿的比我還像新娘。我一直安慰自己滓彰,他們只是感情好控妻,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揭绑,像睡著了一般弓候。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上他匪,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天菇存,我揣著相機與錄音,去河邊找鬼邦蜜。 笑死依鸥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悼沈。 我是一名探鬼主播贱迟,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼絮供!你這毒婦竟也來了衣吠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤壤靶,失蹤者是張志新(化名)和其女友劉穎缚俏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡袍榆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了塘揣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片包雀。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖亲铡,靈堂內(nèi)的尸體忽然破棺而出才写,到底是詐尸還是另有隱情,我是刑警寧澤奖蔓,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布赞草,位于F島的核電站,受9級特大地震影響吆鹤,放射性物質(zhì)發(fā)生泄漏厨疙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一疑务、第九天 我趴在偏房一處隱蔽的房頂上張望沾凄。 院中可真熱鬧,春花似錦知允、人聲如沸撒蟀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽保屯。三九已至,卻和暖如春涤垫,著一層夾襖步出監(jiān)牢的瞬間姑尺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工雹姊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留股缸,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓吱雏,卻偏偏與公主長得像敦姻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子歧杏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容