A Java Android In-App Purchasing Tutorial

In the previous chapter, we explored how to integrate in-app purchasing into an Android project and also looked at some code samples that can be used when working on your own projects. This chapter will put this theory into practice by creating an example project that demonstrates how to add a consumable in-app product to an Android app. The tutorial will also show how in-app products are added and managed within the Google Play Console and explain how to enable test payments so that purchases can be made during testing without having to spend real money.

About the In-App Purchasing Example Project

The simple concept behind this project is an app in which an in-app product must be purchased before a button can be clicked. This in-app product is consumed each time the button is clicked, requiring the user to re-purchase the product each time they want to be able to click the button. On initialization, the app will connect to the app store, obtain details of the product, and display the product name. Once the app has established that the product is available, a purchase button will be enabled which, when clicked, will step through the purchase process. On completion of the purchase, the main button will be enabled so that the user can click on it and consume the purchase.

Creating the InAppPurchase Project

The first step in this exercise is to create a new project. Begin by launching Android Studio, selecting the New Project option from the welcome screen. In the new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter InAppPurchase into the Name field and specify a package name that will uniquely identify your app within the Google Play ecosystem (for example com.<your company>.InAppPurchase). Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java. Once the project has been created, use the steps outlined in section 11.8 Migrating a Project to View Binding to convert the project to use view binding.

Adding Libraries to the Project

Before we start writing code, some libraries need to be added to the project build configuration, one of which is the standard Android billing client library. Later in the project, we will also need to use the ImmutableList class which is part of Google’s Guava Core Java libraries. Add these libraries now by modifying the Gradle Scripts -> build.gradle (Module: InAppPurchase.app) file with the following changes:

.
.
dependencies {
.
.
    implementation 'com.android.billingclient:billing:5.0.0'
    implementation 'com.google.guava:guava:24.1-jre'
    implementation 'com.google.guava:guava:27.0.1-android'
.
.

After adding the library, click on the Sync Now link at the top of the editor panel to commit the changes.

Designing the User Interface

The user interface will consist of the existing TextView and two Buttons. With the activity_main.xml file loaded into the editor, drag and drop two Button views onto the layout so that one is above and the other below the TextView. Select the TextView and change the id attribute to statusText.

Click on the Clear all Constraints button in the toolbar and shift-click to select all three views. Right-click on the top-most Button view and select the Center -> Horizontally in Parent menu option. Repeat this step once more, this time selecting Chains -> Create Vertical Chain. Change the text attribute of the top button so that it reads “Consume Purchase”, the id to consumeButton, and set the enabled property to false. Also, configure the onClick property to call a method named consumePurchase.

Select the bottom-most button and repeat the above steps, this time setting the text to “Buy Product”, the id to buyButton, the onClick callback to makePurchase, and the enabled property to false. Once completed, the layout should match that shown in Figure 84-1:

Figure 84-1

Adding the App to the Google Play Store

Using the steps outlined in the chapter entitled “Creating, Testing and Uploading an Android App Bundle”, sign into the Play Console, create a new app, and set up a new internal testing track including the email addresses of designated testers. Return to Android Studio and generate a signed release app bundle for the project. Once the bundle file has been generated, upload it to the internal testing track and roll it out for testing.

Now that the app has a presence in the Google Play Store, we are ready to create an in-app product for the project.

Creating an In-App Product

With the app selected in the Play Console, scroll down the list of options in the left-hand panel until the Monetize section comes into view. Within this section, select the In-app products option listed under Products as shown in Figure 84-2:

Figure 84-2

On the In-app products page, click on the Create product button:

Figure 84-3

On the new product screen, enter the following information before saving the new product:

  • Product ID: one_button_click
  • Name: A Button Click
  • Description: This is a test in-app product that allows a button to be clicked once.
  • Default price: Set to the lowest possible price in your preferred currency.

Enabling License Testers

When testing in-app billing it is useful to be able to make test purchases without spending any money. This can be achieved by enabling license testing for the internal track testers. License testers can use a test payment card when making purchases so that they are not charged.

Within the Play Console, return to the main home screen and select the Setup -> License testing option:

Figure 84-4

Within the license testing screen, add the testers that were added for the internal testing track, change the License response setting to RESPOND_NORMALLY, and save the changes:

Figure 84-5

Now that both the app and the in-app product have been set up in the Play Console, we can start adding code to the project.

Initializing the Billing Client

Edit the MainActivity.java file and make the following changes to begin implementing the in-app purchase functionality:

.
.
import androidx.annotation.NonNull;

import android.util.Log;
import com.android.billingclient.api.*;
.
.
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private BillingClient billingClient;
    private ProductDetails productDetails;
    private Purchase purchase;

    static final String TAG = "InAppPurchaseTag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
        billingSetup();
    }

    private void billingSetup() {

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

        billingClient.startConnection(new BillingClientStateListener() {

            @Override
            public void onBillingSetupFinished(
                       @NonNull BillingResult billingResult) {

                if (billingResult.getResponseCode() ==  
                          BillingClient.BillingResponseCode.OK) {
                    Log.i(TAG, "OnBillingSetupFinish connected");
                    queryProduct();
                } else {
                    Log.i(TAG, "OnBillingSetupFinish failed");
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                Log.i(TAG, "OnBillingSetupFinish connection lost");
            }
        });
    }
.
.

When the app starts, the onCreate() method will now call billingSetup() which will, in turn, create a new billing client instance and attempt to connect to the Google Play Billing Library. The onBillingSetupFinished() listener will be called when the connection attempt completes and output Logcat messages indicating the success or otherwise of the connection attempt. Finally, we have also implemented the onBillingServiceDisconnected() callback which will be called if the Google Play Billing Library connection is lost.

If the connection is successful a method named queryProduct() is called. Both this method and the purchasesUpdatedListener assigned to the billing client now need to be added.

Querying the Product

To make sure the product is available for purchase, we need to create a QueryProductDetailsParams instance configured with the product ID that was specified in the Play Console, and pass it to the queryProductDetailsAsync() method of the billing client. This will require that we also add the onProductDetailsResponse() callback method where we will check that the product exists, extract the product name, and display it on the status TextView. Now that we have obtained the product details, we can also safely enable the buy button. Within the MainActivity.java file, add the queryProduct() method so that it reads as follows:

.
.
import com.google.common.collect.ImmutableList;

import java.util.List;
.
.
private void queryProduct() {

    QueryProductDetailsParams queryProductDetailsParams =
        QueryProductDetailsParams.newBuilder()
                .setProductList(
                        ImmutableList.of(
                                QueryProductDetailsParams.Product.newBuilder()
                                        .setProductId("one_button_click")
                                        .setProductType(
                                         BillingClient.ProductType.INAPP)
                                            .build()))
                    .build();

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

                    if (!productDetailsList.isEmpty()) {
                        productDetails = productDetailsList.get(0);
                        runOnUiThread(() -> {
                            binding.buyButton.setEnabled(true);
                            binding.statusText.setText(productDetails.getName());
                        });
                    } else {
                        Log.i(TAG, "onProductDetailsResponse: No products");
                    }
                }
            }
    );
}

Much of the code used here should be familiar from the previous chapter. The listener code checks that at least one product was found that matches the query criteria. The ProductDetails object is then extracted from the first matching product, stored in the productDetails variable, and the product name property displayed is on the TextView.

One point of note is that when we enable the buy button and display the product name on the status TextView we do so by calling runOnUiThread(). This is necessary because the listener is not running on the main thread so cannot safely make direct changes to the user interface.

Launching the Purchase Flow

When the user clicks the buy button, a method named makePurchase() will be called to start the purchase process. We can now add this method as follows:

public void makePurchase(View view) {
 
    BillingFlowParams billingFlowParams =
        BillingFlowParams.newBuilder()
            .setProductDetailsParamsList(
                ImmutableList.of(
                    BillingFlowParams.ProductDetailsParams.newBuilder()
                        .setProductDetails(productDetails)
                        .build()
                 )
            )
            .build();

    billingClient.launchBillingFlow(this, billingFlowParams);
}

Handling Purchase Updates

The results of the purchase process will be reported to the app via the PurchaseUpdatedListener that was assigned to the billing client during the initialization phase. Add this handler now as follows:

private final PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, 
                            List<Purchase> purchases) {

        if (billingResult.getResponseCode() == 
                         BillingClient.BillingResponseCode.OK
                && purchases != null) {
            for (Purchase purchase : purchases) {
                completePurchase(purchase);
            }
        } else if (billingResult.getResponseCode() ==
               BillingClient.BillingResponseCode.USER_CANCELED) {
            Log.i(TAG, "onPurchasesUpdated: Purchase Canceled");
        } else {
            Log.i(TAG, "onPurchasesUpdated: Error");
        }
    }
};

The handler will output log messages if the user cancels the purchase or another error occurs. A successful purchase, however, results in a call to a method named completePurchase() which is passed the current Purchase object. Add this method as outlined below:

private void completePurchase(Purchase item) {

    purchase = item;

    if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
        runOnUiThread(() -> {
                binding.consumeButton.setEnabled(true);
                binding.statusText.setText("Purchase Complete");
        });
}

This method stores the purchase before verifying that the product has indeed been purchased and that payment is not still pending. The “consume” button is then enabled and the user is notified that the purchase was successful.

Consuming the Product

With the user now able to click on the “consume” button, the next step is to make sure the product is consumed so that only one click can be performed before another button click is purchased. This requires that we now write the consumePurchase() method:

public void consumePurchase(View view) {
    ConsumeParams consumeParams =
            ConsumeParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken())
                    .build();

    ConsumeResponseListener listener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, 
                  @NonNull String purchaseToken) {
            if (billingResult.getResponseCode() == 
                         BillingClient.BillingResponseCode.OK) {
                runOnUiThread(() -> {
                    binding.consumeButton.setEnabled(false);
                    binding.statusText.setText("Purchase consumed");
                });
            }
        }
    };
    billingClient.consumeAsync(consumeParams, listener);
}

This method creates a ConsumeParams instance and configures it with the purchase token for the current purchase (obtained from the Purchase object previously saved in the completePurchase() method). A handler is declared to disable the consume button and update the status text if the consumption is successful. Finally, the consumeAsync() method of the billing client is called and passed both the consumer parameters and a reference to the listener.

Testing the App

Before we can test the app we need to upload this latest version to the Play Console. As we already have version 1 uploaded, we first need to increase the version number in the build.gradle (Module: InAppPurchase.app) file:

.
.
defaultConfig {
    applicationId "com.ebookfrenzy.inapppurchase"
    minSdk 26
    targetSdk 32
    versionCode 2
    versionName "2.0"
.
.

Sync the build configuration, then follow the steps in the “Creating, Testing and Uploading an Android App Bundle” chapter to generate a new app bundle, upload it to the internal test track and roll it out to the testers. Using the testing track link, install the app on a device or emulator on which one of the test accounts is signed in.

After the app starts it should, after a short delay, display the product name on the TextView and enable the buy button. Clicking the buy button will begin the purchase flow as shown in Figure 84-6:

Figure 84-6

Tap the buy button to complete the purchase using the test card and wait for the Consume Purchase button to be enabled. Before tapping this button, attempt to purchase the product again and verify that it is not possible to do so because you already own the product.

Tap the Consume Purchase button and wait for the “Purchase consumed” message to appear on the TextView. With the product consumed, it should now be possible to purchase it again.

Troubleshooting

If you encounter problems with the purchase, make sure the device is attached to Android Studio, either via a USB cable or WiFi, and select it from within the Logcat panel. Enter InAppPurchaseTag into the Logcat search bar and check the diagnostic output, adding additional Log calls in the code if necessary. For additional information about failures, a useful trick is to access the debug message from BillingResult instances, for example:

.
.
} else if (billingResult.getResponseCode() 
            == BillingClient.BillingResponseCode.USER_CANCELED) {
    Log.i(TAG, "onPurchasesUpdated: Purchase Canceled");
} else {
    Log.i(TAG, billingResult.getDebugMessage());
}

Note that as long as you leave the app version number unchanged in the module-level build.gradle file, you should now be able to run modified versions of the app directly on the device or emulator without having to re-bundle and upload it to the console.

If the test payment card is not listed, make sure the user account on the device has been added to the license testers list. If the app is running on a physical device, try running it on an emulator. If all else fails, you can enter a valid payment method to make test purchases, and then refund yourself using the Order management screen accessible from the Play Console home page.

Summary

In this chapter, we created a project that demonstrated how to add an in-app product to an Android app. This included the creation of the product within the Google Play Console and the writing of code to initialize and connect to the billing client, querying of available products, and, finally, the purchase and consumption of the product. We also explained how to add license testers with the Play Console so that purchases can be made during testing without spending money.

An Overview of Android In-App Billing in Java

In the early days of mobile applications for operating systems such as Android and iOS, the most common method for earning revenue was to charge an upfront fee to download and install the application. Another revenue opportunity was soon introduced in the form of embedding advertising within applications. Perhaps the most common and lucrative option is now to charge the user for purchasing items from within the application after it has been installed. This typically takes the form of access to a higher level in a game, acquiring virtual goods or currency, or subscribing to premium content in the digital edition of a magazine or newspaper.

Google provides support for the integration of in-app purchasing through the Google Play In-App Billing API and the Play Console. This chapter will provide an overview of in-app billing and outline the steps in integrating in-app billing into your Android projects. Once these topics have been explored, the next chapter will walk you through creating an example app that includes in-app purchasing features.

Preparing a Project for In-App Purchasing

Building in-app purchasing into an app will require a Google Play Developer Console account, details of which were covered previously in the “Creating, Testing and Uploading an Android App Bundle” chapter. You will also need to register a Google merchant account and configure your payment settings. These settings can be found by navigating to Setup -> Payments profile in the Play Console. Note that merchant registration is not available in all countries. For details, refer to the following page:

https://support.google.com/googleplay/android-developer/answer/9306917

The app will then need to be uploaded to the console and enabled for in-app purchasing. The console will not activate in-app purchasing support for an app, however, unless the Google Play Billing Library has been added to the module-level build.gradle file as follows:

dependencies {
.
.
    implementation 'com.android.billingclient:billing:<latest version>'
.
.
}

Once the library has been added and the app bundle uploaded to the console, the next step is to add in-app products or subscriptions for the user to purchase.

Creating In-App Products and Subscriptions

Products and subscriptions are created and managed using the options listed beneath the Monetize section of the Play Console navigation panel as highlighted in Figure 83-1 below:

Figure 83-1

Each product or subscription needs an ID, title, description, and pricing information. Purchases fall into the categories of consumable (the item must be purchased each time it is required by the user such as virtual currency in a game), non-consumable (only needs to be purchased once by the user such as content access), and subscription-based. Consumable and non-consumable products are collectively referred to as managed products.

Subscriptions are useful for selling an item that needs to be renewed on a regular schedule such as access to news content or the premium features of an app. When creating a subscription, a base plan is defined specifying the price, renewal period (monthly, annually, etc.), and whether the subscription auto-renews. Users can also be provided with discount offers and given the option of pre-purchasing a subscription.

Billing Client Initialization

Communication between your app and the Google Play Billing Library is handled by a BillingClient instance. In addition, BillingClient includes a set of methods that can be called to perform both synchronous and asynchronous billing-related activities. When the billing client is initialized, it will need to be provided with a reference to a PurchasesUpdatedListener callback handler. The client will call this handler to notify your app of the results of any purchasing activity. To avoid duplicate notifications, it is recommended to have only one BillingClient instance per app.

A BillingClient instance can be created using the newBuilder() method, passing through the current activity or fragment context. The purchase update handler is then assigned to the client via the setListener() method:

private final PurchasesUpdatedListener purchasesUpdatedListener = 
                                     new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(BillingResult billingResult,
                                   List<Purchase> purchases) {

        if (billingResult.getResponseCode() ==
                BillingClient.BillingResponseCode.OK
                && purchases != null) {

            // Purchase(s) successful

            for (Purchase purchase : purchases) {
                // Process purchases
            }
        } else if (billingResult.getResponseCode() ==
                BillingClient.BillingResponseCode.USER_CANCELED) {
            // User cancelled purchase            
        } else {
            // handle erros here
        }
    }
};

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

Connecting to the Google Play Billing Library

After the successful creation of the Billing Client, the next step is to initialize a connection to the Google Play Billing Library. To establish this connection, a call needs to be made to the startConnection() method of the billing client instance. Since the connection is performed asynchronously, a BillingClientStateListener handler needs to be implemented to receive a callback indicating whether the connection was successful. Code should also be added to override the onBillingServiceDisconnected() method. This is called if the connection to the Billing Library is lost and can be used to report the problem to the user and to retry the connection.

Once the setup and connection tasks are complete, the BillingClient instance will make a call to the onBillingSetupFinished() method which can be used to check that the client is ready:

billingClient.startConnection(new BillingClientStateListener() {

    @Override
    public void onBillingSetupFinished(
            @NonNull BillingResult billingResult) {

        if (billingResult.getResponseCode() ==
                BillingClient.BillingResponseCode.OK) {
            // Connection successful
        } else {
            // Connection failed
        }
    }

    @Override
    public void onBillingServiceDisconnected() {
        // Existing connection lost
    }
});

Displaying Available Products

Once the billing environment is initialized and ready to go, the next step is to request the details of the products or subscriptions that are available for purchase. This is achieved by making a call to the queryProductDetailsAsync() method of the BillingClient and passing through an appropriately configured QueryProductDetailsParams instance containing the product ID and type (ProductType.SUBS for a subscription or ProductType.INAPP for a managed product):

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

            if (!productDetailsList.isEmpty()) {
                // Process list of matching products
            } else {
                // No product matches found
            }
        }
    }
);

The queryProductDetailsAsync() method is passed a ProductDetailsResponseListener handler which, in turn, is called and passed a list of ProductDetail objects containing information about the matching products. For example, we can call methods on these objects to get information such as the product name, title, description, price, and offer details.

Starting the Purchase Process

Once a product or subscription has been queried and selected for purchase by the user, the purchase process is ready to be launched. We do this by calling the launchBillingFlow() method of the BillingClient, passing through as arguments the current activity and a BillingFlowParams instance configured with the ProductDetail object for the item being purchased.

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(
            ImmutableList.of(
                BillingFlowParams.ProductDetailsParams.newBuilder()
                        .setProductDetails(productDetails)
                        .build()
            )
        )
        .build();

billingClient.launchBillingFlow(this, billingFlowParams);

The success or otherwise of the purchase operation will be reported via a call to the PurchasesUpdatedListener callback handler outlined earlier in the chapter.

Completing the Purchase

When purchases are successful, the PurchasesUpdatedListener handler will be passed a list containing a Purchase object for each item. You can verify that the item has been purchased by calling the getPurchaseState() method of the Purchase instance as follows:

if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
    // Purchase completed. 
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
    // Paymemt is still pending
}

Note that your app will only support pending purchases if call a call to enablePendingPurchases() method during initialization. A pending purchase will remain so until the user completes the payment process.

When the purchase of a non-consumable item is complete, it will need to be acknowledged to prevent a refund from being issued to the user. This requires the purchase token for the item which is obtained via a call to the getPurchaseToken() method of the Purchase object. This token is used to create an AcknowledgePurchaseParams instance together with an AcknowledgePurchaseResponseListener handler. Managed product purchases and subscriptions are acknowledged by calling the BillingClient’s acknowledgePurchase() method as follows:

AcknowledgePurchaseParams acknowledgePurchaseParams =
        AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchase.getPurchaseToken())
                .build();

AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = 
                   new AcknowledgePurchaseResponseListener() {

    @Override
    public void onAcknowledgePurchaseResponse(
                      @NonNull BillingResult billingResult) {
        // Check acknowledgement result
    }
};

billingClient.acknowledgePurchase(acknowledgePurchaseParams, 
                             acknowledgePurchaseResponseListener);

For consumable purchases, you will need to notify Google Play when the item has been consumed so that it is available to be repurchased by the user. This requires a configured ConsumeParams instance containing a purchase token, a ConsumeResponseListener, and a call to the billing client’s consumeAsync() method:

ConsumeParams consumeParams =
    ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.getPurchaseToken())
            .build();

ConsumeResponseListener listener = new ConsumeResponseListener() {
    @Override
    public void onConsumeResponse(BillingResult billingResult,
                                  @NonNull String purchaseToken) {
        if (billingResult.getResponseCode() ==
                BillingClient.BillingResponseCode.OK) {
            // Purchase consumed successfully            
        }
    }
};

billingClient.consumeAsync(consumeParams, listener);

Querying Previous Purchases

When working with in-app billing it is a common requirement to need to check whether a user has already purchased a product or subscription. A list of all the user’s previous purchases of a specific type (non-consumable or active subscriptions) can be generated by calling the queryPurchasesAsync() method of the BillingClient instance and implementing a PurchaseResponseListener. The following code, for example, obtains a list of all previously purchased non-consumable items:

QueryPurchasesParams queryPurchasesParams =
        QueryPurchasesParams.newBuilder()
                .setProductType(BillingClient.ProductType.INAPP)
                .build();

billingClient.queryPurchasesAsync(queryPurchasesParams, 
                       new PurchasesResponseListener() {
    @Override
    public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, 
              @NonNull List<Purchase> list) {
        // Process list of purchases
    }
});

To obtain a list of active subscriptions, change the ProductType value from INAPP to SUBS.

Alternatively, to obtain a list of past purchases, including active, expired, canceled, and consumed items, make a call to the BillingClient queryPurchaseHistoryAsync() method:

QueryPurchaseHistoryParams queryPurchaseHistoryParams =
        QueryPurchaseHistoryParams.newBuilder()
                .setProductType(BillingClient.ProductType.INAPP)
                .build();

billingClient.queryPurchaseHistoryAsync(queryPurchaseHistoryParams, 
               new PurchaseHistoryResponseListener() {
    
    @Override
    public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, 
               @NonNull List<PurchaseHistoryRecord> list) {
        // Process purchase history
    }
});

Summary

In-app purchases provide a way to generate revenue from within Android apps by selling virtual products and subscriptions to users. In this chapter, we have explored managed products and subscriptions and explained the difference between consumable and non-consumable products. In-app purchasing support is added to an app using the Google Play In-app Billing Library and involves creating and initializing a billing client on which methods are called to perform tasks such as making purchases, listing available products, and consuming existing purchases. The next chapter contains a tutorial demonstrating the addition of in-app purchases to an Android Studio project.