实施 Google Play 结算库版本 2

Implement Google Play Billing Library version 2

Google 发布了一个全新的版本来处理 Android 中的付款,但在搜索了一段时间后,我找不到成功实施它的人的单个示例或教程。

文档很短,只提供了一部分必要的代码: https://developer.android.com/google/play/billing/billing_library_overview

提供的唯一示例是使用 Kotlin 制作的: https://github.com/android/play-billing-samples

他们似乎忘记了 Java 开发人员...

有没有人知道网上的教程或者成功实现了?我当前的代码还远未发布。

这是我在 Kotlin 中使用 billing 2.1.0 的实现。如果您看到整个图片,您可以轻松地将其转换为 Java(这就是我将整个 activity 粘贴给您的原因)。

class GoPremiumActivity : AppCompatActivity(), PurchasesUpdatedListener, AcknowledgePurchaseResponseListener {

    private lateinit var billingClient: BillingClient
    private val skuList = listOf(CStr.PRODUCT_ADS_REMOVE.value)
    private var skuDetails: SkuDetails? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.go_premium)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        setupBillingClient()

        purchaseButton.setOnClickListener {
            val flowParams = BillingFlowParams.newBuilder()
                    .setSkuDetails(skuDetails)
                    .build()
            billingClient.launchBillingFlow(this@GoPremiumActivity, flowParams)
        }
    }


    private fun setupBillingClient() {
        billingClient = BillingClient
                .newBuilder(this@GoPremiumActivity)
                .enablePendingPurchases()
                .setListener(this@GoPremiumActivity)
                .build()

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult?) {
                if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
                    getAvailableProducts()

                    val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
                    val purchase = purchasesResult.purchasesList.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
                    if (purchase?.isAcknowledged == true) {
                        Global.prefs.adsRemovalPurchased = true
                        finish()
                    }
                } else {
                    showGeneralError()
                }
            }

            override fun onBillingServiceDisconnected() {
                /*DO NOTHING*/
            }
        })
    }

    fun getAvailableProducts() {
        if (billingClient.isReady) {
            val params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(skuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build()
            billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
                if (responseCode.responseCode == BillingClient.BillingResponseCode.OK) {
                    skuDetails = skuDetailsList.firstOrNull()
                    skuDetails?.let {
                        purchaseButton.text = String.format("BUY %s", it.price)
                        showSuccessOrError(success = true)
                    } ?: run {
                        showSuccessOrError(success = false)
                    }
                } else {
                    showGeneralError()
                }
            }
        } else {
            showGeneralError()
        }
    }

    override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList<Purchase>?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            val purchase = purchases.firstOrNull { it.sku == CStr.PRODUCT_ADS_REMOVE.value}
            if (purchase?.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (!purchase.isAcknowledged) {
                    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                            .setPurchaseToken(purchase.purchaseToken)
                            .build()
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, this@GoPremiumActivity)
                }
            }
        } else if (billingResult?.responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
            Global.prefs.adsRemovalPurchased = true
            finish()
        } else {
            Global.prefs.adsRemovalPurchased = false
            showSuccessOrError(success = true)
        }
    }

    override fun onAcknowledgePurchaseResponse(billingResult: BillingResult?) {
        if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) {
            showThankYouDialog(this@GoPremiumActivity)
            Global.prefs.adsRemovalPurchased = true
        }
    }

    private fun showSuccessOrError(success: Boolean) {
        purchaseProgressBar.visibility = View.GONE
        if (success) {
            purchaseButton.visibility = View.VISIBLE
        } else {
            purchaseUnavailable.visibility = View.VISIBLE
        }
    }

    private fun showGeneralError() {
        purchaseProgressBar.visibility = View.GONE
        purchaseUnavailable.visibility = View.VISIBLE
    }

    companion object {
        fun newIntent(context: Context): Intent {
            return Intent(context, GoPremiumActivity::class.java)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        finish()
        return true
    }

    public override fun onDestroy() {
        super.onDestroy()
    }

    override fun onPause() {
        super.onPause()
        if (isFinishing) {
            finish()
        }
    }

    private fun showThankYouDialog(context: Context) {
        //Show dialog
    }
}

如果您在 Java 中特别需要它,我可以删除它。

我是 Android Studio 的初学者,我正在实施计费库 2.1.0。在阅读 android studio 文档和许多关于计费库的教程一周后,我做了这个 java class,但我觉得还不够好,至少它做了它必须做的做。如果您发现任何改进方法,请发表评论。感谢:

1.- Class Pago.java:

package com.example.billing;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.Nullable;

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.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.example.R;

import static com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_TIMEOUT;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.OK;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE;
import static com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR;
import static com.android.billingclient.api.BillingClient.SkuType.INAPP;

import java.util.ArrayList;
import java.util.List;


public class Pagos implements PurchasesUpdatedListener, BillingClientStateListener, SkuDetailsResponseListener, ConsumeResponseListener {


    private BillingClient billingClient;
    private Context contextPago;
    private String skuId;
    private List<SkuDetails> misProductos;


    // Constructor de la clase Pagos
    public Pagos(Context context) {

        contextPago = context;

    }


    // Asigna el sku del producto que se quiere comprar
    public void comprar(String skuId) {

        this.skuId = skuId;
        configurarBillingClient();

    }


    // Configura el Billing Client para iniciar la conexión con Google Play Console
    private void configurarBillingClient() {

        //  1. Configura el Billing Client
        billingClient = BillingClient.newBuilder(contextPago)
                .enablePendingPurchases()
                .setListener(this)
                .build();

        // 2. Inicia la conexión y asigna los Listener
        billingClient.startConnection(this);

    }


    @Override
    // Evento salta al llamar billingClient.startConnection()
    public void onBillingSetupFinished(BillingResult billingResult) {

        // Busca compras en el Servidor de Google y las marca como consumidas
        consumeCompras();

        // Verifica que la versión de Play Store sea compatible con INAPP
        if (!billingClient.isReady()) {
            String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_VERSIÓN_NO_COMPATIBLE);
            Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
            return;
        }

        // Verifica que la versión de Play Store sea compatible con Suscripciones
        // if (billingClient.isFeatureSupported(SUBSCRIPTIONS).getResponseCode() != OK) {
        //     String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_VERSIÓN_NO_COMPATIBLE);
        //     Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
        //     return; //GooglePlayNoSoportaComprasDeSuscripciones
        // }

        // Verifica que la Configuración se haya hecho bien, sino muestra mensaje de error
        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            consultaProductos();
        }

    }


    // Asigna los elemento que se consultarán a Google y los envía con querySkuDetailsAsync
    private void consultaProductos() {

        // Inicializa constantes
        String ITEM_SKU_1 = "android.test.item_unavailable";
        String ITEM_SKU_2 = "android.test.canceled";
        String ITEM_SKU_3 = "android.test.purchased";
        String ITEM_SKU_4 = "donar";
        String ITEM_SKU_5 = "prueba.1";

        // Agrega los productos que se consultarán a Google
        List<String> skuList = new ArrayList<>();
        skuList.add(ITEM_SKU_1);
        skuList.add(ITEM_SKU_2);
        skuList.add(ITEM_SKU_3);
        skuList.add(ITEM_SKU_4);
        skuList.add(ITEM_SKU_5);
        // TODO Cambiar el ingreso manual de items por una consulta a servidor propio de backend seguro.

        SkuDetailsParams.Builder skuDetailsParams = SkuDetailsParams
                .newBuilder()
                .setSkusList(skuList)
                .setType(INAPP);

        // Envía consulta a Google y devuelve el listado de productos mediante onSkuDetailsResponse
        billingClient.querySkuDetailsAsync(skuDetailsParams.build(), this);

    }


    @Override
    // Evento salta cuando Google envía los detalles de los Productos en Venta
    public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {

        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            if (skuDetailsList != null) {
                misProductos = skuDetailsList;
                muestraDialogoCompra();
            } else {
                String mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_NO_SKUDETAILSLIST);
                Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
            }
        }

    }


    // Lanza el dialogo de compra de Google
    private void muestraDialogoCompra() {

        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(getSkuIdDetails())
                .build();
        billingClient.launchBillingFlow((Activity) contextPago, flowParams);

    }


    // Obtiene el Producto que se comprará según el Sku ingresado mediante comprar(sku);
    private SkuDetails getSkuIdDetails() {

        if (misProductos == null) return null;
        for (SkuDetails skuProducto : misProductos) {
            if (skuId.equals(skuProducto.getSku())) return skuProducto;
        }
        return null;

    }


    @Override
    // Evento salta cuando se finaliza el Proceso de compra
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> list) {

        if (verificaResponseCode(billingResult.getResponseCode()) == OK) {
            // Validar compra con consulta a Google para evitar ingeniería inversa de hackers
            if (validaCompra()) {
                // Compra confirmada
                Log.i("Pagos", "Compra encontrada en servidor");
            } else {
                // Compra no encontrada: Mensaje de error - Revocar privilegios
                Log.i("Pagos", "Compra no encontrada posible hacker");
            }
            consumeCompras();
        }

    }


    // Valida la compra y Devuelve True si encuentra la compra del usuario en el Servidor de Google
    private boolean validaCompra() {

        List<Purchase> purchasesList = billingClient.queryPurchases(INAPP).getPurchasesList();
        if (purchasesList != null && !purchasesList.isEmpty()) {
            for (Purchase purchase : purchasesList) {
                if (purchase.getSku().equals(skuId)) {
                    return true;
                }
            }
        }
        return false;

    }


    // Busca compras en el Servidor de Google y las marca como consumidas
    private void consumeCompras() {

        Purchase.PurchasesResult queryPurchases = billingClient.queryPurchases(INAPP);
        if (queryPurchases.getResponseCode() == OK) {
            List<Purchase> purchasesList = queryPurchases.getPurchasesList();
            if (purchasesList != null && !purchasesList.isEmpty()) {
                for (Purchase purchase : purchasesList) {
                    ConsumeParams params = ConsumeParams.newBuilder()
                            .setPurchaseToken(purchase.getPurchaseToken())
                            .build();
                    billingClient.consumeAsync(params, this);
                }
            }
        }

    }


    @Override
    // Evento salta cuando se ha consumido un producto, Si responseCode = 0, ya se puede volver a comprar
    public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
        if (billingResult.getResponseCode() == OK) {
            Log.i("Pagos", "Token de Compra: " + purchaseToken + " consumida");
        } else {
            Log.i("Pagos", "Error al consumir compra, responseCode: " + billingResult.getResponseCode());
        }
    }


    @Override
    // Evento salta cuando se pierde la conexión durante una compra
    public void onBillingServiceDisconnected() {
        billingClient.startConnection(this);
    }


    // Verifica que el estado del responseCode sea OK, si no muestra mensaje de Error
    private int verificaResponseCode(int responseCode) {

        if (responseCode == OK) return OK;
        if (responseCode == USER_CANCELED) return USER_CANCELED;

        String mensaje = "";
        switch (responseCode) {
            case SERVICE_TIMEOUT:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_SERVICE_TIMEOUT);
                break;
            case BILLING_UNAVAILABLE:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_BILLING_UNAVAILABLE);
                break;
            case ITEM_UNAVAILABLE:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ITEM_UNAVAILABLE);
                break;
            case ERROR:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ERROR);
                break;
            default:
                mensaje = contextPago.getString(R.string.PAGOS_MENSAJE_ERROR) + " código: " + responseCode;
                break;
        }
        Toast.makeText(contextPago, mensaje, Toast.LENGTH_LONG).show();
        return responseCode;

    }


}

3.- 清单

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />

4.- build.gradle

// Google Play Billing Library
implementation 'com.android.billingclient:billing:2.1.0'

5.- 用法,将此代码放在要显示计费组件的任何位置:

private final String SKU_UNAVAILABLE = "android.test.item_unavailable";
private final String SKU_CANCELED = "android.test.canceled";
private final String SKU_PURCHASED = "android.test.purchased";
private final String SKU_DONAR = "donar";


private void donar() {
    Pagos pagos = new Pagos(this);
    pagos.comprar(SKU_DONAR);
    cargandoDialogoCompra(true);
}

您可以将 SKU_DONAR 更改为 SKU_UNAVAILABLE、SKU_CANCELED、SKU_PURCHASED,因为这些是用于测试目的的项目,正如我所读,没有必要将它们添加到玩控制台

6.- Google 玩游戏机

Presencia en Google Play Store -> Productos integrados en la aplicación -> Productos administrados:

Donación (donar) PEN 9.99

就这些了,请改进我的代码,谢谢大家。

感谢@Webfreak,你对 Kotlin 的回答指引我走向正确的方向。

以下是我为 Java 实现它的方式:

首先将'billingclient'库添加到gradle:

implementation 'com.android.billingclient:billing:X.X.X'

并在清单文件中添加所需的权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />

Activity 必须实现以下接口:

public class MainActivity extends AppCompatActivity implements
        ...
        PurchasesUpdatedListener,
        AcknowledgePurchaseResponseListener {

然后我在 onCreate 方法中初始化计费客户端:

/** IN-APPS PURCHASE */
    private BillingClient mBillingClient;
    private long mLastPurchaseClickTime = 0;
    private List<String> mSkuList = new ArrayList<>();
    private List<SkuDetails> mSkuDetailsList = new ArrayList<>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // AppPrefs is just a standalone class I used to get or set shared preferences easily
        mPrefs = AppPrefs.getInstance(this);


        // Rest of your code ...


        /** IN-APP PURCHASES */
        // Initialize the list of all the in-app product IDs I use for this app
        mSkuList.add(Parameters.UNIT_P1);// NoAdsPurchased
        mSkuList.add(Parameters.UNIT_P2);// CustomizationPurchased
        mSkuList.add(Parameters.UNIT_P3);// ChartsPurchased

        // Initialize the billing client
        setupBillingClient();

        // Apply the upgrades on my app according to the user's purchases
        applyUpgrades();
    }

设置结算客户端的方法在这里,以及我用来从应用程序检索可用的应用程序内产品的方法:

private void setupBillingClient() {
        mBillingClient = BillingClient
                .newBuilder(MainActivity.this)
                .enablePendingPurchases() // Useful for physical stores
                .setListener(MainActivity.this)
                .build();

        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    // Load the available products related to the app from Google Play
                    getAvailableProducts();

                    Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);// Or SkuType.SUBS if subscriptions

                    // Init all the purchases to false in the shared preferences (security prevention)
                    mPrefs.setNoAdsPurchased(false);
                    mPrefs.setCustomizationPurchased(false);
                    mPrefs.setChartsPurchased(false);

                    // Retrieve and loop all the purchases done by the user
                    // Update all the boolean related to the purchases done in the shared preferences
                    if (purchasesResult.getPurchasesList() != null) {
                        for (Purchase purchase : purchasesResult.getPurchasesList()) {
                            if (purchase.isAcknowledged()) {
                                Log.e(TAG, purchase.getSku());

                                switch (purchase.getSku()) {
                                    case Parameters.UNIT_P1:
                                        mPrefs.setNoAdsPurchased(true);
                                        break;
                                    case Parameters.UNIT_P2:
                                        mPrefs.setCustomizationPurchased(true);
                                        break;
                                    case Parameters.UNIT_P3:
                                        mPrefs.setChartsPurchased(true);
                                        break;
                                }
                            }
                        }
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
                // TODO Note: It's strongly recommended that you implement your own connection retry policy and override the onBillingServiceDisconnected() method. Make sure you maintain the BillingClient connection when executing any methods.
                Log.e(TAG, "onBillingServiceDisconnected");
            }
        });
    }

    private void getAvailableProducts() {
        if (mBillingClient.isReady()) {
            SkuDetailsParams params = SkuDetailsParams
                    .newBuilder()
                    .setSkusList(mSkuList)
                    .setType(BillingClient.SkuType.INAPP)
                    .build();

            mBillingClient.querySkuDetailsAsync(params, new SkuDetailsResponseListener() {
                @Override
                public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        mSkuDetailsList = skuDetailsList;
                    }
                }
            });
        }
    }

当用户完成购买时(我允许在我的应用程序中的多个片段上购买),我在主 Activity 上调用此函数(使用接口):

@Override
    public void purchase(String sku) {
        // Mis-clicking prevention, using threshold of 3 seconds
        if (SystemClock.elapsedRealtime() - mLastPurchaseClickTime < 3000){
            Log.d(TAG, "Purchase click cancelled");
            return;
        }
        mLastPurchaseClickTime = SystemClock.elapsedRealtime();

        // Retrieve the SKU details
        for (SkuDetails skuDetails : mSkuDetailsList) {
            // Find the right SKU
            if (sku.equals(skuDetails.getSku())) {
                BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                        .setSkuDetails(skuDetails)
                        .build();
                mBillingClient.launchBillingFlow(MainActivity.this, flowParams);
                break;
            }
        }
    }

这里我实现继承的方法:

@Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
        } else {
            displayError(R.string.inapp_purchase_problem, billingResult.getResponseCode());
        }
    }

    private void handlePurchase(Purchase purchase) {
        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
            // Grant entitlement to the user.
            applyPurchase(purchase);

            // Acknowledge the purchase if it hasn't already been acknowledged.
            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, MainActivity.this);
            }
        }
    }

    @Override
    public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            displayError(R.string.inapp_purchase_success, billingResult.getResponseCode());
        }
    }

我添加的用于在我的应用程序上确认购买的方法:

private void applyPurchase(Purchase purchase) {

        switch (purchase.getSku()) {
            case Parameters.UNIT_P1:
                mPrefs.setNoAdsPurchased(true);
                break;
            case Parameters.UNIT_P2:
                mPrefs.setCustomizationPurchased(true);
                break;
            case Parameters.UNIT_P3:
                mPrefs.setChartsPurchased(true);
                break;
        }

        // I remove the ads right away if purchases
        if(mPrefs.getNoAdsPurchased()) {
            destroyAds();
        }
    }

最后一种方法用于在应用程序上应用所有 upgrades/purchases(以移除广告为例):

private void applyUpgrades() {
        // No ads
        if (mPrefs.getNoAdsPurchased()) {
            destroyAds();
        } else {
            loadAds();
        }

        if (mPrefs.getCustomizationPurchased()) {
            // Allow customization
            // ...
        }

        if (mPrefs.getChartsPurchased()) {
            // Allow charts visualization
            // ...
        }
    }

我想这个解决方案还不完美,但它正在工作,如果我发现改进,我会修改代码。

这是 Google Java 中的 Play Billing 版本 2 的示例应用程序:

Classy Taxi in Java