BillingClient 的 purchasesList 一开始是空的

purchasesList from BillingClient is null at first

我有 BillingHandler class,您可以在下方看到我使用 google 的计费库 v.3 处理应用内计费相关逻辑。我正在使用 Koin 在我的应用程序模块中使用 single { BillingHandler(androidContext()) } 创建单例实例。

现在,当我从我的 SettingsFragment 调用 class' doesUserOwnPremium() 方法时,我的问题出现了,该方法使用设置首选项来显示用作购买按钮的首选项。首先,我使用 get() 访问 billingHandler 实例,然后调用该方法来检查用户是否拥有付费产品。我已经在测试时购买了它,但是当我第一次导航到该片段时,BillingHandler class 中的 purchasesList 为空,因此此 return 为假。单击首选项并尝试启动计费流程后,loadSKUs() 中的处理程序 if(!ownsProduct()) {..} 逻辑被调用并评估为 false,从而通知我我确实拥有它。

loadSKUs()方法和doesUserOwnPremium()方法调用ownsProduct()的时间不同,每次都return以上结果。这是为什么?跟初始化有关系吗?

SettingsFragment.kt:

class SettingsFragment : SharedPreferences.OnSharedPreferenceChangeListener
    , PreferenceFragmentCompat() {

    private val TAG = SettingsFragment::class.java.simpleName

    // Billing library setup
    private lateinit var billingHandler:BillingHandler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        purchasePremiumPreference = findPreference(resources.getString(R.string.purchase_premium))!!
        purchasePremiumPreference.isEnabled = false // disable until the client is ready
        billingHandler = get()

        val ownsPremium = billingHandler.doesUserOwnPremium()
        Toast.makeText(requireContext(),"owns product = $ownsPremium",Toast.LENGTH_LONG).show()
        if(!ownsPremium) {
            purchasePremiumPreference.isEnabled = true
            purchasePremiumPreference.setOnPreferenceClickListener {
                billingHandler.startConnection()
                true
            }
        }
    }
}

BillingHandler.kt:

/**
 * Singleton class that acts as an abstraction layer on top of the Billing library V.3 by google.
 */
class BillingHandler(private val context: Context) : PurchasesUpdatedListener {

    // Billing library setup
    private var billingClient: BillingClient
    private var skuList:ArrayList<String> = ArrayList()
    private val sku = "remove_ads" // the sku to sell
    private lateinit var skuDetails: SkuDetails // the details of the sku to sell
    private var ownsPremium = false

    fun doesUserOwnPremium() : Boolean = ownsPremium

    // analytics
    private lateinit var firebaseAnalytics: FirebaseAnalytics

    init {
        skuList.add(sku) // add SKUs to the sku list (only one in this case)
        billingClient = BillingClient.newBuilder(context)
            .enablePendingPurchases()
            .setListener(this)
            .build()
        ownsPremium = ownsProduct()
    }

    /**
     * Attempts to establish a connection to the billing client. If successful,
     * it will attempt to load the SKUs for sale and begin a billing flow if needed.
     * If the connection fails, it will prompt the user to either retry the connection
     * or cancel it.
     */
    fun startConnection() {
        // start the connection
        billingClient.startConnection(object:BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    loadSKUs()
                } else {
                    Toast.makeText(context,"Something went wrong, please try again!",
                        Toast.LENGTH_SHORT).show()
                }
            }

            override fun onBillingServiceDisconnected() {
                Toast.makeText(context,"Billing service disconnected!", Toast.LENGTH_SHORT).show()
                TODO("implement retry policy. Maybe using a dialog w/ retry and cancel buttons")
            }

        })
    }

    /**
     * Loads the skus from the skuList and starts the billing flow
     * for the selected sku(s) if needed.
     */
    private fun loadSKUs() {
        if(billingClient.isReady) { // load the products that the user can purchase
            val skuDetailsParams = SkuDetailsParams.newBuilder()
                .setSkusList(skuList)
                .setType(BillingClient.SkuType.INAPP)
                .build()

            billingClient.querySkuDetailsAsync(skuDetailsParams
            ) { billingResult, skuDetailsList ->
                if(billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null && skuDetailsList.isNotEmpty()) {
                    // for each sku details object
                    for(skuDetailsObj in skuDetailsList) {
                        // make sure the sku we want to sell is in the list and do something for it
                        if(skuDetailsObj.sku == sku) {
                            if(!ownsProduct()) { // product not owned
                                skuDetails = skuDetailsObj // store the details of that sku
                                startBillingFlow(skuDetailsObj)
                            } else { // give premium benefits
                                Toast.makeText(context,"You already own Premium!",Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                }
            }
        } else {
            Toast.makeText(context,"Billing client is not ready. Please try again!",Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * Checks whether or not the user owns the desired sku
     * @return True if they own the product, false otherwise
     */
    private fun ownsProduct(): Boolean {
        var ownsProduct = false
        // check if the user already owns this product
        // query the user's purchases (reads them from google play's cache)
        val purchasesResult: Purchase.PurchasesResult =
            billingClient.queryPurchases(BillingClient.SkuType.INAPP)

        val purchasesList = purchasesResult.purchasesList // get the actual list of purchases

        if (purchasesList != null) {
            for (purchase in purchasesList) {
                if (purchase.sku == sku) {
                    ownsProduct = true
                    break
                }
            }
        } else {
            Toast.makeText(context,"Purchases list was null",Toast.LENGTH_SHORT).show()
        }
        return ownsProduct
    }

    /**
     * Starts the billing flow for the purchase of the desired
     * product.
     * @param skuDetailsObj The SkuDetails object of the selected sku
     */
    private fun startBillingFlow(skuDetailsObj:SkuDetails) {
        val billingFlowParams = BillingFlowParams.newBuilder()
            .setSkuDetails(skuDetailsObj)
            .build()

        billingClient.launchBillingFlow(
            context as Activity,
            billingFlowParams
        )
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchasesList: MutableList<Purchase>?) {
        if(billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
            purchasesList != null) {
            for(purchase in purchasesList) {
                handlePurchase(purchase)
            }
        }
    }

    /**
     * Handles the given purchase by acknowledging it if needed .
     * @param purchase The purchase to handle
     */
    private fun handlePurchase(purchase: Purchase) {
        // if the user purchased the desired sku
        if(purchase.sku == sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            if(!purchase.isAcknowledged) { // acknowledge the purchase so that it doesn't get refunded
                val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
                    .build()
                billingClient.acknowledgePurchase(acknowledgePurchaseParams
                ) { billingResult ->
                    if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // log the event using firebase
                        // log event to firebase
                        val eventBundle = Bundle()
                        eventBundle.putString(FirebaseAnalytics.Param.ITEM_ID,"purchase_ack")
                        eventBundle.putString(FirebaseAnalytics.Param.ITEM_NAME,"Purchase acknowledged")
                        eventBundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, "IN_APP_PURCHASES")
                        firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE,eventBundle)
                    }
                }
            }
            showPurchaseSuccessDialog()
        }
    }

    /**
     * Shows a dialog to inform the user of the successful purchase
     */
    private fun showPurchaseSuccessDialog() {
        MaterialDialog(context).show {
            title(R.string.premium_success_dialog_title)
            message(R.string.premium_success_dialog_msg)
            icon(R.drawable.ic_premium)
        }
    }
}

再次挖掘和阅读文档后,我意识到当我第一次调用 ownsProduct() 方法时,计费客户端的 startConnection() 方法尚未被调用,因此为什么查询返回 null,客户端尚未准备好。

我决定通过简单地使用以下方法开始虚拟连接来绕过它,以便从我的应用程序 class 中设置客户端。这样,当用户到达应用程序的任何位置时,客户端就准备好了,我可以获得 his/hers 实际购买清单。

fun dummyConnection() {
    billingClient.startConnection(object : BillingClientStateListener {
        override fun onBillingSetupFinished(p0: BillingResult) {
        }

        override fun onBillingServiceDisconnected() {
        }

    })
}

我猜这可能会产生不良的副作用,所以我很想就这是否是正确的方法获得一些反馈。顺便说一句,我需要客户尽快准备好,因为我希望能够验证他们在整个应用程序中拥有付费(以禁用广告等)。