问题
Google published a brand new version to handle the payments in Android but after searching quite a while I cannot find a single example or tutorial from someone who succeeded implementing it.
The documentation is very short and provides only a part of the necessary code: https://developer.android.com/google/play/billing/billing_library_overview
The only sample provided is made with Kotlin: https://github.com/android/play-billing-samples
Seems like they forgot about Java developers...
Does anyone know a tutorial online or succeeded to implement it? My current code is far from working to be published yet.
回答1:
Thanks @Webfreak, your answer for Kotlin guided to me to the right direction.
Here is how I implemented it for Java:
First add the 'billingclient' library to gradle :
implementation 'com.android.billingclient:billing:X.X.X'
And add the required permissions in the manifest file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />
The Activity must implements the following interfaces:
public class MainActivity extends AppCompatActivity implements
...
PurchasesUpdatedListener,
AcknowledgePurchaseResponseListener {
Then I initialize the billing client inside the onCreate method:
/** 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();
}
The method setting up the billing client is here, along with the metyhod I use to retrieve the avaialble in-app products fro the app:
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;
}
}
});
}
}
When a purchase is done by the user (I allow purchases on several Fragments in my app), I call this function on the main Activity (using an interface):
@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;
}
}
}
Here I implement the methods inherited:
@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());
}
}
The method I added to acknowledge a purchase on my app:
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();
}
}
This last method is used to apply all the upgrades/purchases on the app (with an example with the removal of the ads):
private void applyUpgrades() {
// No ads
if (mPrefs.getNoAdsPurchased()) {
destroyAds();
} else {
loadAds();
}
if (mPrefs.getCustomizationPurchased()) {
// Allow customization
// ...
}
if (mPrefs.getChartsPurchased()) {
// Allow charts visualization
// ...
}
}
I guess this solution is not perfect yet but it is working, I will modify the code if I find improvements.
回答2:
Here is my implementation using billing 2.1.0 in Kotlin. You can easily convert it to Java if you see the whole picture (that's why I'm pasting you the entire 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
}
}
I can remove it if you specifically want it in Java.
回答3:
I'm begginer on Android Studio and i'm implementing the billing library 2.1.0. After a week of reading the android studio documentation and many tutorial about billing library I have made this java class, but i feel is not good enough, at least it do what it has to do. If you find any way to improve it, comment it. Thanks:
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.- Manifest
<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.- Usage, place this code wherever you want to show the billing component:
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);
}
You can change SKU_DONAR, to SKU_UNAVAILABLE, SKU_CANCELED, SKU_PURCHASED because these are items for testing purposes and as i read its not neccesary to add them to play console
6.- Google Play console
Presencia en Google Play Store -> Productos integrados en la aplicación -> Productos administrados:
Donación (donar) PEN 9.99
That's all, please improve my code, thanks to you all.
回答4:
Here is a sample app for Google Play Billing version 2 in Java:
Classy Taxi in Java
来源:https://stackoverflow.com/questions/59735936/implement-google-play-billing-library-version-2