2024. április 27., szombat

Gyorskeresés

Android/Google BillingClient 5.x példaprogram

Írta: | Kulcsszavak: android . alkalmazás . fejlesztés . gányolás . billingclient

[ ÚJ BEJEGYZÉS ]

A Google (számos egyéb bűneik mellett :DDD ) novembertől kötelezővé tette a Play-en szereplő alkalmazásoknak az 5.x Billingclient használatát, valamint a 33-as SDK (Android 13) célzását új alkalmazásoknak és frissítéseknek. (És hamarosan 6.x Billingclient lesz a kötelező.)

Mivel nekem a Barcodenote-ban és az AID-ban is az Anjlab-féle 3-as Billingclient library volt, amiből nem volt újabb, muszáj volt túrni egy 5-ös Billingclient megvalósítást (mert magamtól nyilván sügér vagyok összerakni :O ), amit a Github-on Wicaodian oldalán meg is lehetett lelni (azaz inkább példát a netúr Billingcient használatára). Összedobtam az alapján (=abból) egy tesztet (vigyázat, borzalmas tákolmány alant!), hogy lássam, egyáltalán hogy működik, mielőtt elkezdem széttúrni a két alkalmazást. (Meg persze nekem csak annyi kellett, hogy egy "terméket" azaz adományt meg lehessen venni, tehát nem kellett consumable, stb. -el szórakozni.)

A példában ennyi is lesz; egy terméket meg lehet benne venni, ennyit csináltam meg az alkalmazásokba is, beépített adakozási céllal (kb. a fejlesztés fele ez volt időban mind a kettőnél, hagyni kellett volna a francba :D ). (Viszont semmi egyéb hibakezelés, vagy ilyesmi, tehát ha nincs Play Services az eszközön, vagy el sem indul, vagy a vásárlás hatására összedől :D - ezt már nem nehéz megoldani, hogy ne tegye.)

A legnagyobb változás az, hogy már nem kell külön kulcs az alkalmazásnak; intézi magától a kliens az alkalmazás egyéb azonosítói alapján; viszont a teszteléshez be kell regisztrálni az alkalmazást egy fejlesztői fiókban, létrehozni megvehető terméket, bétaként feltölteni, és hozzáadni egy személyt (akár magunkat) bétatesztelőnek. (Szerencsére továbbra sem tudjuk legatyásítani magunkat, mert ilyenkor egy tesztkártyát szimulál a kliens :D )


Rettenetesen bonyolult


És ez van, ha rányomunk a gombra

MainActivity :
//https://github.com/wicaodian/Google-In-App-Billing-Library-V5-Example/blob/main/IapSample/app/src/main/java/com/memo/iapsample/Consumable.java

//app neve
package com.test.billing5test;

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

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

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.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.example.billing5test.R;
import com.test.billing5test.MainActivity;

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

//MainActivity
public class MainActivity extends AppCompatActivity {

//A logban ezt keresd
private String TAG = "iapSample";

//A kliens
private BillingClient billingClient;

//A vásárlás gomb
Button button;

//Végül nem használtam, Wicaodian tesztjében van szerepe
TextView textView;

//Itt ugyanazokat kell megadni, amiket beregisztráltunk a Play Console-n - nem igazán értem, miért, hiszen majd le is tölti a lehetséges termékeket
private final String PRODUCT_PREMIUM = "lifetime";
private final String NoAds = "NoAds";
private ArrayList<String> purchaseItemIDs = new ArrayList<String>(2) {{
add(PRODUCT_PREMIUM);
add(NoAds);
}};



//A Resume-ban nem csinálunk semmit
@Override
protected void onResume() {
super.onResume();

}

//A program indulásakor
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//Felépíti a klienst
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
.setListener(
(billingResult, list) -> {

if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
for (Purchase purchase : list) {

Log.d(TAG, "Response is OK"+list.toString());
handlePurchase(purchase);
}
} else {

Log.d(TAG, "Response NOT OK");
}
}
).build();

//És ha megvan a kliens, akkor a kapcsolatot is
establishConnection();
init();
}

void init() {

//Létrehozza a gombot az UI-n
button = this.findViewById(R.id.button);
//És kattintásra meghívja a GetSingleInAppDetail() -t
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.d(TAG, "Click on button");
GetSingleInAppDetail();
}

});

}

//Kapcsolat felépítése a Billing Service-el
void establishConnection() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {

//Itt van készen a kapcsolat a BillingService felé. Egy termék vásárlásához a GetSingleInAppDetail(); használható

Log.d(TAG, "Connection Established");
}
}

@Override
public void onBillingServiceDisconnected() {
// A következő vásárlásnál, ha megszűnt a kapcsolat, megpróbál újrakapcsolódni
Log.d(TAG, "Connection NOT Established");
establishConnection();
}
});
}



//Amikor rányomtunk a gombra, ez fut le
void GetSingleInAppDetail() {
Log.d(TAG, "GetSingleInappDetail");
ArrayList<QueryProductDetailsParams.Product> productList = new ArrayList<>();

//A setProductId() -nak megmondjuk, hogy a "PRODUCT_PREMIUM" - ot lehet majd megvenni (ez a lifetime nevű termék a Play Console-n), és leszedi a termék tulajdonságait.
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(PRODUCT_PREMIUM)
.setProductType(BillingClient.ProductType.INAPP)
.build()
);

QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();

billingClient.queryProductDetailsAsync(params, new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(@NonNull BillingResult billingResult, @NonNull List<ProductDetails> list) {

//Itt bármit lehet tenni a terméktulajdonságokkal, pl. saját formon listázni, stb., de ugye itt csak 1 termék van, így felesleges

//A LaunchPurchaseFlow indítja a kliens vásárlási párbeszédablakát; ezt már a Play Services hozza fel.
//Calling this function here so that once products are verified we can start the purchase behavior.
//You can save this detail in separate variable or list to call them from any other location
//Create another function if you want to call this in establish connections' success state
Log.d(TAG, "Starting Purchaseflow");
LaunchPurchaseFlow(list.get(0));

}
});
}

//Ez már a vásárlási felületet indíja
void LaunchPurchaseFlow(ProductDetails productDetails) {
Log.d(TAG, "Purchaseflow started");
ArrayList<BillingFlowParams.ProductDetailsParams> productList = new ArrayList<>();

productList.add(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build());

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productList)
.build();
Log.d(TAG, "Billingflow started");
billingClient.launchBillingFlow(this, billingFlowParams);
}

//Ez pedig lekezeli
void handlePurchase(Purchase purchases) {
if (!purchases.isAcknowledged()) {
billingClient.acknowledgePurchase(AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchases.getPurchaseToken())
.build(), billingResult -> {

//Ha rendben ment a vásárlás
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (String pur : purchases.getProducts()) {
if (pur.equalsIgnoreCase(PRODUCT_PREMIUM)) {
Log.d("TAG", "Purchase is successful");
textView.setText("Yay! Purchased");
//Itt hozzá lehet tolni egy else ágon azt, hogy dobjon üzenetet, ha nem sikerült.

//Ha consumable a cucc, akkor a ConsumePurchase()-el lehet felhasználni
// so user will be able to buy same product again
//ConsumePurchase(purchases);
}
}
}
});
}
}

}

Manifest :
<?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.Billing5test"
tools:targetApi="33">
<activity
android:name="com.test.billing5test.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>

Build.Gradle :
plugins {
id("com.android.application")
}

android {
namespace = "com.example.billing5test"
compileSdk = 33

defaultConfig {
applicationId = "com.test.billing5test"
minSdk = 26
targetSdk = 33
versionCode = 1
versionName = "1.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
}
}

dependencies {
implementation("com.android.billingclient:billing:5.2.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

Layout 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="Billingcilent 5 test"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.561" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Vásárolj"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

A Play Console-n pedig :


Létrehozni egy alkalmazást...


...létrehozni termékeket (a neve ugyanaz legyen, mint a MainActivity elején a listában)...


...lefordítani és signature-zni az alkalmazást, feltölteni bétatesztre ("Belső tesztelés"...


...a belső teszteléshez hozzáadni egy fejlesztői fókunkat, vagy végülis akárkit.

A fenti folyamat nekem eltartott pár napig (mármint a program összetákolása, alkalmazás felkalapálása a Play-re), szóval remélhetőleg segítség lesz valakinek ez a bejegyzés is, hogy kevesebbet szívjon egy hasonló cuccal :D

Amúgy remélhetőleg a 6-os Billingclient-re átállás kb. annyi lesz, hogy a Gradle-ben átírni a verziót :D (A 34-es SDK is biztos hoz majd pár szívatást... :O :Y :W )

Hozzászólások

(#1) joghurt


joghurt
addikt

Ha a 33-as API-nak már megfelelsz, akkor a 34-es valószínűleg nem lesz számodra sem túl nagy sokk. A mi appunkat én egy órán belül felhúztam rá mindennel együtt.
Leginkább abból lehet necc, hogyan azonosítod/tárolod, hogy valaki jogosult a prémium programverzióra.
Ja, és ne lepődj meg, hogy a verzióemeléssel a "Támogatott androidos eszközök" száma ugyanannyi marad, mintha egyetlen eszközön sem futna még Android 14.

A Google részéről az egyhetes átfutási idő a mai világban totál gyalázatos. Ember szerintem tipikusan nincs a folyamatban egy ilyen kaliberű alkalmazásnál, scriptek nézegetik az API hívásokat, és ez alapján végeznek kockázatelemzést.

A tej élet, erő, egészség.

(#2) hcl válasza joghurt (#1) üzenetére


hcl
félisten
LOGOUT blog

"Ja, és ne lepődj meg, hogy a verzióemeléssel a "Támogatott androidos eszközök" száma ugyanannyi marad, mintha egyetlen eszközön sem futna még Android 14."
Ez zavar a legkevésbé :D

Prémium nincs; ha valakinek tetszik a cucc, akkor adhat adományt, és ennyi. Ez 1db ~$1 értékű "termék".

A 34-es remélem nem lesz nagy sokk :D Asszem a Java 1.8 már kell, meg még valami, amivel lesz macera.

Mutogatni való hater díszpinty

(#3) joghurt válasza hcl (#2) üzenetére


joghurt
addikt

Az Android Studio megaszondik mindent.

A tej élet, erő, egészség.

(#4) hcl válasza joghurt (#3) üzenetére


hcl
félisten
LOGOUT blog

Meg a Gugli, Stackoverflow... :)

Mutogatni való hater díszpinty

További hozzászólások megtekintése...
Copyright © 2000-2024 PROHARDVER Informatikai Kft.