Google Billingclient 8.x Android példaprogram

Miután már a sokadik Google Billingclient verzióváltás van (évente ugrik a kötelező verzió :W ) , megint összedobtamösszekókányoltam egy tesztprogit, amiben van egy működő kliens. És ha már megtettem, miért ne tegyem közkinccsé :D ? Mármint sokat nem tudok a működéséről, de legalább megy; inicializálni kell, van az a pár paraméter, amit össze kell rakni a számlázókliens hívása előtt, és ha megfelelő termékazonosítót kap, akkor a megfelelő hívásokkal lemegy a vásárlás. Sokat nem kell variálni vele. Meg persze kell lennie a Play-en egy regisztrált alkalmazásnak a megfelelő névvel, és ahhoz létrehozva termékeknek, amiknek az azonosítóit kell paraméterlistaként átadni.
Nyilván nekem ez annyiban érdekes, hogy egy $1-1,5 értékű, adománynak felvett termék van a programjaimhoz, mert sajnos a Google nem tűri, hogy Play-ben regisztrált alkamazásban másképpen lehessen a fejlesztőnek adakozni (pl. mezei Paypal link), így kénytelen vagyok karbantartani ezt is :O :DDD . Amúgy nyilván lehet "előfizetés" meg consumable típusú dolgokat is felvenni, pl. játékokban van értelme. (Update : később láttam, hogy a Google kiadott teszteszközt is a Billingclienthez, a Play Billing Lab alkalmazással elég sokféle esetet szimulálhatunk a sikeres/sikertelen vásárlások között, konkrétan a kliensnek érkező lehetséges válaszokat lehet megadni .)

Szóval remélem, másnak is hasznos lesz - a progi alant, meg pár képernyőkép a működéséről.

Nyilván az app neve azért billing5test, mert annak idején az 5-öshöz ezen a néven volt a teszt app, és a megvehető adomány is ahhoz van létrehozva; csak emiatt elég macera lett volna még egy alkalmazást akár csak tesztfázisig felregisztrálni a Play-re. A lényeges pont a GetSingleInAppDetail(), azt lehet rátenni valamilyen vezérlőelemre, és ha meghívjuk, akkor indul a vásárlás.)

MainActivity.java
package com.test.billing5test;

Hirdetés

import static android.content.ContentValues.TAG;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.android.billingclient.api.AcknowledgePurchaseParams;
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.PendingPurchasesParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryProductDetailsResult;

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

public class MainActivity extends AppCompatActivity {
public TextView textview;

//Definiáljuk a BillingClient-et
private BillingClient billingClient;

//Létre kell hozni a "terméket" is, ugyanazzal az ID-el, mint a Play-en
private final String DONATION = "1d";

//Terméklista tömb
private ArrayList<String> purchaseItemIDs = new ArrayList<String>(2) {{
add(DONATION);
}};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//UI elemek
Button button = findViewById(R.id.button);
textview=findViewById(R.id.textView);

//A gomb eseménykezelője
button.setOnClickListener( new View.OnClickListener() {

@Override
public void onClick(View v) {
GetSingleInAppDetail();

}

});

//Billingclient inicializálás
billingClient = BillingClient.newBuilder(this)

.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
.setListener(
(billingResult, list) -> {

if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
for (Purchase purchase : list) {
Toast.makeText(getApplicationContext(),"Billingclient response OK",Toast.LENGTH_SHORT).show();
Log.d(TAG, "Response is OK" + list.toString());
handlePurchase(purchase);
}
} else {
Toast.makeText(getApplicationContext(),"Purchase failed",Toast.LENGTH_SHORT).show();
Log.d(TAG, "Purchase failed");
}
}
).build();

establishConnection();
}

void establishConnection() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {

//Ha eddig eljutott, akkor már van kapcsolat a Google BillingService-el

//Elvileg ezekkel lehet vásárlást kezdeményezni, vagy az elérhető termékeket lekérdezni. Sosem próbáltam :D
// GetSingleInAppDetail();
//GetListsInAppDetail();

Log.d(TAG, "Connection Established");
} else {
Log.d("TAG", "Billingclient connection FAILED");
Toast.makeText(getApplicationContext(),"Error during establishing Google Billing connection",Toast.LENGTH_SHORT).show();
}
}

//Ha nincs kapcsolat, akkor próbáljon meg kapcsolódni :D
@Override
public void onBillingServiceDisconnected() {
Log.d(TAG, "Connection NOT Established");
establishConnection();
}
});
}

void GetSingleInAppDetail() {
Log.d(TAG, "GetSingleInappDetail");
ArrayList<QueryProductDetailsParams.Product> productList = new ArrayList<>();

//Felépíti a termékek listáját
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(DONATION)
.setProductType(BillingClient.ProductType.INAPP)
.build()
);

QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder().setProductList(
List.of(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(DONATION)
.setProductType(BillingClient.ProductType.INAPP)
.build()
)
)
.build();

billingClient.queryProductDetailsAsync(params, new ProductDetailsResponseListener() {
@Override

public void onProductDetailsResponse(BillingResult billingResult,
QueryProductDetailsResult productDetailsResult) {
Log.d(TAG, "Purchaseflow");
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
List<ProductDetails> productDetailsList = productDetailsResult.getProductDetailsList();
if (productDetailsList != null && !productDetailsList.isEmpty()) {
ProductDetails productDetails = productDetailsList.get(0);

//Ez indítja a vásárlást
LaunchPurchaseFlow(productDetails);

} else {
Log.d(TAG,"ProductDetailsList is null");
Toast.makeText(getApplicationContext(),"ProductDetailsList is null",Toast.LENGTH_SHORT).show();
}
}
}

});
}

void LaunchPurchaseFlow(ProductDetails productDetails) {
Log.d(TAG, "PurchaseFlow started");
//Valamiért ez a toast megakasztja
//Toast.makeText(getApplicationContext(),"PurchaseFlow started",Toast.LENGTH_SHORT).show();
ArrayList<BillingFlowParams.ProductDetailsParams> productList = new ArrayList<>();

//Összerakja a paramétereket a konkrét vásárláshoz
productList.add(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build());

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productList)
.build();
Log.d(TAG, "BillingFlow started");
Log.d(TAG, String.valueOf(productList));
//Meg ez is
//Toast.makeText(getApplicationContext(),"BillingFlow started",Toast.LENGTH_SHORT).show();

//És itt indítja a számlázást, amit már a Billing csinál
billingClient.launchBillingFlow(this, billingFlowParams);
}

//Elvileg ez kezeli le a vásárlás végeredményét
void handlePurchase(Purchase purchases) {

if (!purchases.isAcknowledged()) {
billingClient.acknowledgePurchase(AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchases.getPurchaseToken())
.build(), billingResult -> {

if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (String pur : purchases.getProducts()) {
if (pur.equalsIgnoreCase(DONATION)) {
Log.d("TAG", "Purchase is successful");
Toast.makeText(getApplicationContext(),"Thanks for your donation :) ",Toast.LENGTH_SHORT).show();
textview.setText("Yay! Purchased");

//Ha valami elhasználható termékről van szó, akkor ezzel lehet majd felhasználni - sosem próbáltam :D
//ConsumePurchase(purchases);
}
}
}
else {Log.d("TAG", "Purchase FAILED");
Toast.makeText(getApplicationContext(),"Error during Google Billing call",Toast.LENGTH_SHORT).show();
}
});
}
}

Activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@android:dimen/app_icon_size"
android:text="Consume"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Build.gradle (app level)
plugins {
id("com.android.application")
}

android {
namespace = "com.test.billing5test"
compileSdk = 34

defaultConfig {
applicationId = "com.test.billing5test"
minSdk = 29
targetSdk = 34
versionCode = 2
versionName = "2.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

buildFeatures{
dataBinding = true
viewBinding = true
}
}

dependencies {

implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.android.billingclient:billing:8.1.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Billing6test"
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

A beregisztrált "termék" (az 1d érdekes, így neveztem el a kb. $1 értékű adományt, ugyanezzel kell hivatkozni rá a programban is) :

És akkor lehet a tesztkártyával vásárolni :D

...de csak egyszer - a termék tulajdonságainál állítható, hogy pl. többet is lehessen venni.

A végén jön egy számla is :