Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - Google Play Billing Library error in production

I have an Android app released on the Google Play Store, and last week I released a new update, just to fix some small issues. Starting with the day when I added the updated version on the Play Store, I could see on Firebase Crashlytics that there are issues when someone is trying to purchase an app feature.

Before I released the updated version in production, I added the app on the Alpha Testing so I can make sure that the InAppPurchase work, and it does.

When someone else is trying to purchase an app feature I can see that this Fatal Exception is thrown:

Fatal Exception: java.lang.IllegalArgumentException: SKU cannot be null.
   at com.android.billingclient.api.BillingFlowParams$Builder.build(com.android.billingclient:billing@@3.0.0:23)

The SKU's are still active on my "Managed Products" list.

This is the code that I use to initialize the billing client (within a fragment):

        billingClient = BillingClient.newBuilder(getActivity())
            .enablePendingPurchases()
            .setListener(purchasesUpdatedListener)
            .build();

This is the code that I use to start the connection:

billingClient.startConnection(new BillingClientStateListener() {
        @Override
        public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
            Log.d(TAG, "Connection finished");
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                // The BillingClient is ready. You can query purchases here.
                List<String> skuList = new ArrayList<>();
                skuList.add("unlock_keyboard");
                SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
                params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
                billingClient.querySkuDetailsAsync(params.build(),
                        new SkuDetailsResponseListener() {
                            @Override
                            public void onSkuDetailsResponse(@NonNull BillingResult billingResult,
                                                             List<SkuDetails> skuDetailsList) {
                                // Process the result.
                                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
                                    for (Object skuDetailsObject : skuDetailsList) {
                                        skuDetails = (SkuDetails) skuDetailsObject;
                                        sku = skuDetails.getSku();


                                    }
                                    Log.d(TAG, "i got response");
                                    Log.d(TAG, String.valueOf(billingResult.getResponseCode()));
                                    Log.d(TAG, billingResult.getDebugMessage());
                                }
                            }
                        });
            }
        }

This is the code that I use to handle the purchase:

PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
        @Override
        public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list != null) {
                for (Purchase purchase : list) {
                    handlePurchase(purchase);
                    Log.d(TAG, "Purchase completed" + billingResult.getResponseCode());
                }
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
                Log.d(TAG, "User Canceled" + billingResult.getResponseCode());
            } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
                if ("unlock_keyboard".equals(sku)) {
                    KeyboardAlreadyPurchasedConfirmation();
                }
                Log.d(TAG, "Item Already owned" + billingResult.getResponseCode());
            }
        }
    };

In order to launch the billing flow, the user must click on a button within a dialog. Here is the code:

        builder.setPositiveButton(
            getString(R.string.purchase_keyboard),
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id) {

                    sku = "unlock_keyboard";
                    BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                            .setSkuDetails(skuDetails)
                            .build();
                    billingClient.launchBillingFlow(Objects.requireNonNull(getActivity()), flowParams);

                }

            });

In the previous version of my app, this situation never happened, it just started after the new update. I'll just need to know what can cause this issue, could it be a problem with my code or just a issue with Google Play Services? I'll have to specify that this was happening on different devices with different Android versions.

Thanks a lot in advance.

like image 322
Alexandru Dumitru Avatar asked Jul 12 '20 06:07

Alexandru Dumitru


1 Answers

I have not solved the issue yet but I found a way to reduce the numbers of errors generated by this library upgrade.

What I did was to downgrade the Google Billing Library from version 3.0.1 to version 2.1.0 and even though I still get some errors in Firebase (SKU is null), the majority of users are now able to purchase the products.

Also, I implemented a method that is called whenever the Google Billing library connection cannot be started when the activity is first opened, so more exactly this is restart billing connection method.

I would recommend you to try the same thing at least for now if you are experiencing the same issue because it seems that the Google Billing library still has some issues that need to be fixed.

1. In build.gradle(app) add this line:

implementation 'com.android.billingclient:billing:2.1.0'

2. Add the BILLING permission in AndroidManifest.xml file because the older versions of this library still require it:

<uses-permission android:name="com.android.vending.BILLING" />

3. Create a restart billing connection method:

public void restartBillingConnection() {
    billingClient = BillingClient.newBuilder(Objects.requireNonNull(getActivity())).enablePendingPurchases().setListener(ChooseOptionsFragment.this).build();

    billingClient.startConnection(new BillingClientStateListener() {@Override
    public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
        Log.d(TAG, "Connection finished");
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            // The BillingClient is ready. You can query purchases here.
            List < String > skuList = new ArrayList < >();
            skuList.add(ITEM_SKU_AD_REMOVAL);
            SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
            params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
            billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {@Override
            public void onSkuDetailsResponse(@NonNull BillingResult billingResult, List < SkuDetails > skuDetailsList) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList != null) {
                    for (Object skuDetailsObject: skuDetailsList) {
                        skuDetails = (SkuDetails) skuDetailsObject;
                        sku = skuDetails.getSku();
                        String price = skuDetails.getPrice();
                        if (ITEM_SKU_AD_REMOVAL.equals(sku)) {
                            skuPrice = price;
                            BillingFlowParams flowParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build();
                            billingClient.launchBillingFlow(Objects.requireNonNull(Objects.requireNonNull(getActivity())), flowParams);
                        }
                        else {
                            Log.d(TAG, "Sku is null");
                        }

                    }
                    Log.d(TAG, "i got response");
                    Log.d(TAG, String.valueOf(billingResult.getResponseCode()));
                    Log.d(TAG, billingResult.getDebugMessage());
                }
                else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ERROR) {
                    Toast.makeText(getActivity(), "Error in completing the purchase!", Toast.LENGTH_SHORT).show();
                }
            }
            });
        }

        else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_TIMEOUT) {
            Toast.makeText(getActivity(), "Service timeout!", Toast.LENGTH_SHORT).show();
        }
        else {
            Toast.makeText(getActivity(), "Failed to connect to the billing client!", Toast.LENGTH_SHORT).show();
        }

    }@Override
    public void onBillingServiceDisconnected() {
        restartBillingConnection();
    }
    });
}

4. Make sure that this method is called when the Google Billing service gets disconnected:

@Override
    public void onBillingServiceDisconnected() {
        restartBillingConnection();
    }

Hope that this solution will help you to fix the issue for now. If you will have another way to have it completely fixed please leave an answer in this post.

like image 88
Alexandru Dumitru Avatar answered Oct 05 '22 23:10

Alexandru Dumitru