Android Studio - 应用内购买:billingclient 问题

Android Studio - In App Purchase: billingclient problem

提问人:Saland 提问时间:11/18/2023 最后编辑:Saland 更新时间:11/18/2023 访问量:77

问:

几年前,我为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 消息:.onUpgradeButtonClickSkuDetails not available

我在支持 Google Play 商店的模拟器 Pixel 7 API 34 上运行它,并且我已登录到 Google Play 商店。

有什么建议吗?

代码更新如下

所以我尝试了一种不同的方法。我一直在关注YouTube视频。为了进行测试,现在实际显示应用内购买名称,但按 .启动或按下时不会显示 Toast,文本也不会显示任何内容。这是我得到的:upgradeButtonupgradeButtoniapTextView

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();
        }
    }
}
应用内 Android 计费 Google-Play计费

评论


答:

0赞 Harshali 11/18/2023 #1

根据官方的 google-in-app-billing 文档,您应该,

  • 替换为SkuDetailsParamsQueryProductDetailsParams
  • 切换通话以使用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();
        }
    });
}

更新后,您的对象将更改为queryProductDetailsAsyncmyProductSkuDetailsProductDetails

 private ProductDetails myProductSkuDetails; // Define SkuDetails field

有关详细信息,请参阅 integrate-google-in-app-billingMigration-to-goggle-play-billing-v6 官方文档。

评论

0赞 Saland 11/18/2023
不知何故,它仍然给了我信息。SkuDetails not available
0赞 Harshali 11/18/2023
@Saland,你确定你得到了回应吗?请首先验证您获得的产品详细信息。queryProductDetailsAsyncproductDetailsListqueryProductDetailsAsync
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()startConnectionBillingClient