提问人:Saland 提问时间:11/18/2023 最后编辑:Saland 更新时间:11/18/2023 访问量:77
Android Studio - 应用内购买:billingclient 问题
Android Studio - In App Purchase: billingclient problem
问:
几年前,我为Android制作了一个应用程序,但该项目被错误地删除了。又过了几年,我决定再写一遍。所以我来到了我想添加应用购买(删除广告)的部分,但不知何故它没有按计划工作。我尝试过按照文档并在线搜索,但没有运气。
我在我的应用程序build.gradle中有这些:
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.test.espresso:espresso-core:3.5.1")
val billing_version = "6.1.0"
implementation("com.android.billingclient:billing:$billing_version")
implementation("com.android.billingclient:billing-ktx:$billing_version")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
这是我的 Java 类的样子:
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
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.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class InfoController extends AppCompatActivity implements PurchasesUpdatedListener {
private Button upgradeButton;
// Billing variables
private BillingClient billingClient;
private PurchasesUpdatedListener purchasesUpdatedListener;
private SkuDetails myProductSkuDetails; // Define SkuDetails field
List<String> skuList = Arrays.asList("com.xxxxxxx.yyyyyyyyyyyyy.pro");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info);
upgradeButton = findViewById(R.id.upgradeButton);
billingClient = BillingClient.newBuilder(this)
.setListener(this)
.enablePendingPurchases()
.build();
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The billing client is ready, query SkuDetails here
querySkuDetails();
} else {
// Handle the error
Toast.makeText(InfoController.this, "Billing setup failed: " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onBillingServiceDisconnected() {
// Handle the case when the billing service is disconnected
Toast.makeText(InfoController.this, "Billing service disconnected", Toast.LENGTH_SHORT).show();
}
});
}
private void querySkuDetails() {
// Query SkuDetails for your product
List<String> skuList = Arrays.asList("com.xxxxxxx.yyyyyyyyyyyyy.pro");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
Log.d("BillingDebug", "Before querySkuDetailsAsync");
billingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
Log.d("BillingDebug", "Inside querySkuDetailsAsync callback");
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
// Use the first SkuDetails object
Toast.makeText(this, "Billing OK", Toast.LENGTH_SHORT).show();
myProductSkuDetails = skuDetailsList.get(0);
} else {
Toast.makeText(this, "Failed to retrieve SKU details", Toast.LENGTH_SHORT).show();
}
});
Log.d("BillingDebug", "After querySkuDetailsAsync");
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// Implement your logic here when purchases are updated
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
// Handle the purchase
Toast.makeText(this, "OK", Toast.LENGTH_SHORT).show();
}
} else {
// Handle an error
Toast.makeText(this, "ERROR", Toast.LENGTH_SHORT).show();
}
}
public void onUpgradeButtonClick(View view) {
if (myProductSkuDetails != null) {
Toast.makeText(this, "Not null", Toast.LENGTH_SHORT).show();
// Create a BillingFlowParams object
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(myProductSkuDetails)
.build();
// Launch the billing flow
BillingResult result = billingClient.launchBillingFlow(this, billingFlowParams);
if (result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
// Handle the error
}
} else {
// SkuDetails not available, handle accordingly
Toast.makeText(this, "SkuDetails not available", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (billingClient != null) {
billingClient.endConnection();
}
}
}
问题:
因此,当我加载此活动时,它打开正常,没有错误,也没有 Toast 消息。但是当我按下 时,它会给我 Toast 消息:.onUpgradeButtonClick
SkuDetails not available
我在支持 Google Play 商店的模拟器 Pixel 7 API 34 上运行它,并且我已登录到 Google Play 商店。
有什么建议吗?
代码更新如下:
所以我尝试了一种不同的方法。我一直在关注YouTube视频。为了进行测试,现在实际显示应用内购买名称,但按 .启动或按下时不会显示 Toast,文本也不会显示任何内容。这是我得到的:upgradeButton
upgradeButton
iapTextView
package com.xxxxxxx.yyyyyyyyyyyyy.droid;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.test.espresso.core.internal.deps.guava.collect.ImmutableList;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
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.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class InfoController extends AppCompatActivity {
private Button upgradeButton;
private TextView iapTextView;
// Billing variables
private BillingClient billingClient;
String subsName,des;
Boolean isSuccess = false;
Boolean isPro = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info);
upgradeButton = findViewById(R.id.upgradeButton);
iapTextView = findViewById(R.id.iapTextView);
iapTextView.setVisibility(View.VISIBLE);
iapTextView.setTextColor(getResources().getColor(R.color.white));
iapTextView.setBackgroundColor(getResources().getColor(R.color.blue));
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
getPrice();
}
private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// To be implemented in a later section.
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED){
Toast.makeText(InfoController.this, "ITEM_ALREADY_OWNED", Toast.LENGTH_SHORT).show();
iapTextView.setText("ITEM_ALREADY_OWNED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) {
Toast.makeText(InfoController.this, "FEATURE_NOT_SUPPORTED", Toast.LENGTH_SHORT).show();
iapTextView.setText("FEATURE_NOT_SUPPORTED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
Toast.makeText(InfoController.this, "BILLING_UNAVAILABLE", Toast.LENGTH_SHORT).show();
iapTextView.setText("BILLING_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
Toast.makeText(InfoController.this, "USER_CANCELED", Toast.LENGTH_SHORT).show();
iapTextView.setText("USER_CANCELED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.DEVELOPER_ERROR) {
Toast.makeText(InfoController.this, "DEVELOPER_ERROR", Toast.LENGTH_SHORT).show();
iapTextView.setText("DEVELOPER_ERROR");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE) {
Toast.makeText(InfoController.this, "ITEM_UNAVAILABLE", Toast.LENGTH_SHORT).show();
iapTextView.setText("ITEM_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.NETWORK_ERROR) {
Toast.makeText(InfoController.this, "NETWORK_ERROR", Toast.LENGTH_SHORT).show();
iapTextView.setText("NETWORK_ERROR");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
Toast.makeText(InfoController.this, "SERVICE_DISCONNECTED", Toast.LENGTH_SHORT).show();
iapTextView.setText("SERVICE_DISCONNECTED");
} else {
Toast.makeText(InfoController.this, "Error: " + billingResult.getDebugMessage(), Toast.LENGTH_SHORT).show();
}
}
};
void handlePurchase(final Purchase purchase) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = (billingResult, s) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
}
};
billingClient.consumeAsync(consumeParams,listener);
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams,acknowledgePurchaseResponseListener);
iapTextView.setText("You have purchased PRO");
} else {
iapTextView.setText("Already purchased!");
}
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
iapTextView.setText("PURCHASE PENDING");
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE) {
iapTextView.setText("UNSPECIFIED_STATE");
}
}
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
iapTextView.setText("You just got PRO!");
}
}
};
private boolean verifyValidSignature(String signedData, String signature) {
return Security.verifyPurchase(signedData, signature);
/*
try {
// String base64Key = "";
// return Security.verifyPurchase(base64Key, signedData, signature);
return Security.verifyPurchase(signedData, signature);
} catch (IOException e) {
return false;
}
*/
}
private void getPrice() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
QueryProductDetailsParams.Product product = QueryProductDetailsParams.Product.newBuilder()
.setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
.setProductType(BillingClient.ProductType.INAPP)
.build();
productList.add(product);
QueryProductDetailsParams queryProductDetailsParams =
QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
new ProductDetailsResponseListener() {
public void onProductDetailsResponse(BillingResult billingResult,
List<ProductDetails> productDetailsList) {
for (ProductDetails productDetails:productDetailsList) {
String productID = productDetails.getProductId();
subsName = productDetails.getName();
des = productDetails.getDescription();
String formattedPrice = productDetails.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
}
}
}
);
}
});
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
upgradeButton.setText("Name: "+subsName);
}
});
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
}
public void onUpgradeButtonClick(View view) {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingServiceDisconnected() {
}
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
Toast.makeText(InfoController.this, "Pressed", Toast.LENGTH_SHORT).show();
QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder().setProductList(
Arrays.asList(QueryProductDetailsParams.Product.newBuilder()
.setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
.setProductType(BillingClient.ProductType.INAPP)
.build())
).build();
billingClient.queryProductDetailsAsync(
queryProductDetailsParams,
new ProductDetailsResponseListener() {
public void onProductDetailsResponse(BillingResult billingResult,
List<ProductDetails> productDetailsList) {
for (ProductDetails productDetails : productDetailsList) {
String offerToken = productDetails.getSubscriptionOfferDetails().get(0).getOfferToken();
// Create a list with a single element
List<BillingFlowParams.ProductDetailsParams> paramsList = Arrays.asList(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
);
// Use the list in BillingFlowParams
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(paramsList)
.build();
billingClient.launchBillingFlow(InfoController.this, billingFlowParams);
}
}
}
);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (billingClient != null) {
billingClient.endConnection();
}
}
}
答:
0赞
Harshali
11/18/2023
#1
根据官方的 google-in-app-billing 文档,您应该,
- 替换为
SkuDetailsParams
QueryProductDetailsParams
- 切换通话以使用
BillingClient.querySkuDetailsAsync()
BillingClient.queryProductDetailsAsync()
更新功能querySkuDetails()
private void querySkuDetails() {
// QueryProductDetailsParams for your product
QueryProductDetailsParams.Product inAppProduct = QueryProductDetailsParams.Product.newBuilder()
.setProductId("com.xxxxxxx.yyyyyyyyyyyyy.pro")
.setProductType(BillingClient.ProductType.INAPP)
.build();
List<QueryProductDetailsParams.Product> productList = Arrays.asList(inAppProduct);
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(params, (billingResult, productDetailsList) -> {
// productDetailsList will be List<ProductDetails>
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && !productDetailsList.isEmpty()) {
// Use the first productDetailsList object
Toast.makeText(this, "Billing OK", Toast.LENGTH_SHORT).show();
myProductSkuDetails = productDetailsList.get(0);
} else {
Toast.makeText(this, "Failed to retrieve SKU details", Toast.LENGTH_SHORT).show();
}
});
}
更新后,您的对象将更改为queryProductDetailsAsync
myProductSkuDetails
ProductDetails
private ProductDetails myProductSkuDetails; // Define SkuDetails field
有关详细信息,请参阅 integrate-google-in-app-billing 和 Migration-to-goggle-play-billing-v6 官方文档。
评论
0赞
Saland
11/18/2023
不知何故,它仍然给了我信息。SkuDetails not available
0赞
Harshali
11/18/2023
@Saland,你确定你得到了回应吗?请首先验证您获得的产品详细信息。queryProductDetailsAsync
productDetailsList
queryProductDetailsAsync
0赞
Saland
11/18/2023
如果我这样做: ,Toast 消息永远不会出现。if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { // The billing client is ready, query SkuDetails here Toast.makeText(InfoController.this, "Ready", Toast.LENGTH_SHORT).show(); querySkuDetails(); }
0赞
Harshali
11/18/2023
@Saland,似乎您的函数没有被调用,您必须显示 toast“计费设置失败:”对吗?它显示了什么错误?querySkuDetails()
0赞
Harshali
11/18/2023
@Saland,首先验证您在初始化或连接中可能发出的响应。onBillingSetupFinished()
startConnection
BillingClient
评论