This chapter will show you how to create a new Material Design 3 theme using the Material Theme Builder tool and integrate it into an Android Studio project. The tutorial will also demonstrate how to add support for and test dynamic theme colors to an app.
Creating the ThemeDemo Project
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.
Enter ThemeDemo into the Name field and specify com.ebookfrenzy.themedemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.
Designing the User Interface
The main activity will consist of a simple layout containing some user interface components that will enable us to see the effects of the theming work performed later in the chapter. For information on MD3 components, refer to the following web page:
The layout will be designed within the activity_main.xml file, which currently contains a single Text view. Open this file in the layout editor, delete the Text view, turn off Autoconnect mode (marked A in Figure 92-1), and click on the button to clear all constraints from the layout (B).
Figure 92-1
From the Buttons section of the Palette, drag Chip, CheckBox, Switch, and Button views onto the layout canvas. Next, drag a FloatingActionButton onto the layout canvas to position it beneath the Button component. When prompted to choose an icon to appear on the FloatingActionButton, select the ic_lock_power_off icon from within the resource tool window, as illustrated in Figure 92-2:
Figure 92-2
Change the text attribute for the Chip widget to “This is my chip” and set the chipIcon attribute to @ android:drawable/ic_btn_speak_now so that the layout resembles that shown to the left in Figure 92-3:
Figure 92-3
To set up the constraints, select all the components, right-click on the Chip view, and select Chains -> Create Vertical Chain from the resulting menu. Repeat this step, this time selecting the Center -> Horizontally in Parent menu option.
Compile and run the app on a device or emulator and verify that the user interface matches that shown in Figure 92-3 above. The next step is to create a custom theme and apply it to the project.
Building a New Theme
Begin by opening a browser window and navigating to the following URL to access the builder tool:
Once you have loaded the builder, select a wallpaper followed by the Custom button at the top of the screen, and then click on the Primary color circle in the Core Colors section to display the color selector. From the color selector, choose any color as the basis for your theme:
Figure 92-4
Review the color scheme in the Your Theme panel and make any necessary color adjustments using the Core colors settings until you are happy with the color slots. Once the theme is ready, click the Export button in the top right-hand corner and select the Android Views (XML) option. When prompted, save the file to a suitable location on your computer filesystem. The theme will be saved as a compressed file named material-theme.zip.
Using the appropriate tool for your operating system, unpack the theme file, which should contain the following folders and files in a folder named material-theme:
values/colors.xml – The color definitions.
values/themes.xml – The theme for the light mode.
values-night/themes.xml – The theme for dark mode.
Now that the theme files have been generated, they need to be integrated into the Android Studio project.
Adding the Theme to the Project
Before adding the new theme to the project, we first need to remove the old theme files. This is easier if the Project tool window is in Project Files mode. To switch mode, use the menu at the top of the tool Project tool window as shown below and select the Project Files option:
Figure 92-5
With Project Files mode selected, navigate to the app -> src -> main -> res -> values folder and select and delete the colors.xml and themes.xml files. Also, delete the themes.xml file located in the values-night folder.
Open the filesystem navigation tool for your operating system, locate the colors.xml and themes.xml files in the values folder of the new material theme, and copy and paste them into the values folder within the Project tool window. Repeat this step to copy the themes.xml file in the values-night folder, this time pasting it into the values-night folder.
Switch the Project tool window back to Android mode, at which point the value resource files section should match Figure 92-6:
Figure 92-6
Next, modify the light themes.xml file to match the current project as follows:
Repeat these steps to make the same modifications to the themes.xml (night) file.
Return to the activity_main.xml file or rerun the app to confirm that the user interface is rendered using the custom theme colors.
Enabling Dynamic Color Support
The app will need to be run on a device or emulator running Android 12 or later with the correct Wallpaper settings to test dynamic colors. On the device or emulator, launch the Settings app and select Wallpaper & style from the list of options. On the wallpaper settings screen, click the option to change the wallpaper (marked A in Figure 92-7) and select a wallpaper image containing colors that differ significantly from the colors in your theme. Once selected, assign the wallpaper to the Home screen.
Return to the Wallpaper & styles screen and make sure that the Wallpaper colors option is selected (B) before choosing an option from the color scheme buttons (C). As each option is clicked, the wallpaper example will change to reflect the selection:
Figure 92-7
To enable dynamic colors, we need to call the applyToActivitiesIfAvailable() method of the DynamicColors class. To enable dynamic color support for the entire app, this needs to be called from within the onCreate() method of a custom Application instance. Begin by adding a new Kotlin class file to the project under app -> java -> com. ebookfrenzy.themedemo named ThemeDemoApplication.kt and modifying it so that it reads as follows:
With the custom Application class created, we must configure the project to use this class instead of the default Application instance. To do this, edit the AndroidManifest.xml file and add an android:name element referencing the new class:
Build and run the app and note that the layout uses a theme matching the wallpaper color. Place the ThemeDemo app into the background, return to the Wallpaper & styles settings screen, and choose a different wallpaper. Bring the ThemeDemo app to the foreground again. At this point, it will have dynamically adapted to match the new wallpaper.
Previewing Dynamic Colors
Dynamic color behavior can also be previewed within the Android Studio layout editor. To try this, open the activity_main.xml file, click on the theme menu, and select the More Themes option, as shown in Figure 92-8:
Figure 92-8
Next, use the search field in the theme selection dialog to list dynamic themes:
Figure 92-9
Select the Material3.DynamicColors.DayNight theme before clicking on the OK button. On returning to the layout editor, select the System UI Mode menu and choose one of the wallpaper options as highlighted in Figure 92-10:
Figure 92-10
Once a wallpaper has been selected, the colors of the components in the layout will change accordingly.
Summary
In this chapter, we have used the Material Theme Builder to design a new theme and explained the steps to integrate the generated theme files into an Android Studio project. Finally, the chapter demonstrated how to implement and use the Material You dynamic colors feature.
The appearance of an Android app is intended to conform to a set of guidelines defined by Material Design. Google developed Material Design to provide a level of design consistency between different apps while also allowing app developers to include their own branding in terms of color, typography, and shape choices (a concept referred to as Material theming). In addition to design guidelines, Material Design also includes a set of UI components for use when designing user interface layouts, many of which we have used throughout this book.
This chapter will provide an overview of how theming works within an Android Studio project and explore how the default design configurations provided for newly created projects can be modified to meet your branding requirements.
Material Design 2 vs. Material Design 3
Before beginning, it is important to note that Google is transitioning from Material Design 2 to Material Design 3 and that Android Studio Giraffe projects default to Material Design 3. Material Design 3 provides the basis for Material You, a feature introduced in Android 12 that allows an app to automatically adjust theme elements to complement preferences configured by the user on the device. For example, dynamic color support provided by Material Design 3 allows the colors used in apps to adapt automatically to match the user’s wallpaper selection.
Understanding Material Design Theming
We know that Android app user interfaces are created by assembling components such as layouts, text fields, and buttons. These components appear using default colors unless we specifically override a color attribute in the XML layout resource file or by writing code. The project’s theme defines these default colors. The theme consists of a set of color slots (declared in themes.xml files) which are assigned color values (declared in the colors.xml file). Each UI component is programmed internally to use theme color slots as the default color for specific attributes (such as the foreground and background colors of the Text widget). It follows, therefore, that we can change the application-wide theme of an app by changing the colors assigned to specific theme slots. When the app runs, the new default colors will be used for all widgets when the user interface is rendered.
Material Design 3 Theming
Before exploring Material Design 3, we must consider how it is used in an Android Studio project. The theme used by an application project is declared as a property of the application element within the AndroidManifest.xml file, for example:
As previously discussed, all of the files associated with the project theme are contained within the colors.xml and themes.xml files located in the res -> values folder, as shown in Figure 91-1:
Figure 91-1
The theme itself is declared in the two themes.xml files located in the themes folder. These resource files declare different color palettes containing Material Theme color slots for use when the device is in light or dark (night) mode. Note that the style name property in each file must match that referenced in the AndroidManifest.xml file, for example:
<resourcesxmlns:tools="http://schemas.android.com/tools"><!-- Base application theme. --><stylename="Base.Theme.MyDemoApp"parent="Theme.Material3.DayNight.NoActionBar"><!-- Customize your light theme here. --><!-- <item name="colorPrimary">@color/my_light_primary</item> --></style><stylename="Theme.MyDemoApp"parent="Base.Theme.MyDemoApp" /></resources>Code language:HTML, XML(xml)
These color slots (also referred to as color attributes) are used by the Material components to set colors when they are rendered on the screen. For example, the colorPrimary color slot is used as the background color for the Material Button component.
Color slots in MD3 are grouped as Primary, Secondary, Tertiary, Error, Background, and Surface. These slots are further divided into pairs consisting of a base color and an “on” base color. This generally translates to the background and foreground colors of a Material component.
The particular group used for coloring will differ between widgets. A Material Button widget, for example, will use the colorPrimary base color for the background color and colorOnPrimary for its content (i.e., the text or icon it displays). The FloatingActionButton component, on the other hand, uses colorPrimaryContainer as the background color and colorOnPrimaryContainer for the foreground. The correct group for a specific widget type can usually be identified quickly by changing color settings in the theme files and reviewing the rendering in the layout editor.
Suppose that we need to change colorPrimary to red. We achieve this by adding a new entry to the colors.xml file for the red color and then assigning it to the colorPrimary slot in the themes.xml file. The colorPrimary slot in an MD3 theme night, therefore, read as follows:
<resourcesxmlns:tools="http://schemas.android.com/tools"><!-- Base application theme. --><stylename="Base.Theme.MyDemoApp"parent="Theme.Material3.DayNight.NoActionBar"><itemname="colorPrimary">@color/my_bright_primary</item></style><stylename="Theme.MyDemoApp"parent="Base.Theme.MyDemoApp" /></resources>
This color is then declared in the colors.xml file:
<?xml version="1.0" encoding="utf-8"?><resources>
.
.
<colorname="my_bright_primary">#FC0505</color></resources>Code language:HTML, XML(xml)
Building a Custom Theme
As we have seen, the coding work in implementing a theme is relatively simple. The difficult part, however, is often choosing complementary colors to make up the theme. Fortunately, Google has developed a tool that makes it easy to design custom color themes for your apps. This tool is called the Material Theme Builder and is available at:
On the custom screen (Figure 91-2), make a color selection for the primary color key (A) by clicking on the color circle to display the color selection dialog. Once a color has been selected, the preview (B) will change to reflect the recommended colors for all MD3 color slots, along with example app interfaces and widgets. In addition, you can override the generated colors for the Secondary, Tertiary, and Neutral slots by clicking on the corresponding color circles to display the color selection dialog.
The area marked B displays example app interfaces, light and dark color scheme charts, and widgets that update to preview your color selections. Since the panel is longer than the typical browser window, you must scroll down to see all the information.
To incorporate the theme into your design, click the Export button (C) and select the Android View (XML) option. Once downloaded, the colors.xml and themes.xml files can be used to replace the existing files in your project. Note that the theme name in the two exported themes.xml files must be changed to match your project.
Figure 91-2
Summary
Material Design provides guidelines and components defining how Android apps appear. Individual branding can be applied to an app by designing themes that specify the colors, fonts, and shapes used when displaying the app. Google recently introduced Material Design 3, which replaces Material Design 2 and supports the new features of Material You, including dynamic colors. Google also provides the Material Theme Builder for designing your own themes, which eases the task of choosing complementary theme colors. Once this tool has been used to design a theme, the corresponding files can be exported and used within an Android Studio project.
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 demonstrating 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 to make purchases 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 repurchase the product each time they want to be able to click the button. On initialization, the app will connect to the app store, obtain product details, and display the product name. Once the app has established that the product is available, a purchase button will be enabled, which will step through the purchase process when clicked. Upon completion of the purchase, a second button will be enabled so 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. Launch Android Studio and select the New Project option from the welcome screen. Choose the Empty Views Activity template in the new project dialog before clicking the Next button.
Enter InAppPurchase into the Name field and specify a package name that uniquely identifies 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 Kotlin. Once the project has been created, use the steps outlined in section 18.8 Migrating a Project to View Binding to convert the project to use view binding.
Adding Libraries to the Project
Before writing code, some libraries must be added to the project build configuration, including the standard Android billing client library. Later in the project, we will also need to use the ImmutableList class, part of Google’s Guava Core Java libraries. Add these libraries now by modifying the Gradle Scripts -> build.gradle.kts (Module: app) file with the following changes:
Click on the Sync Now link at the top of the editor panel to commit these 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, selecting Chains -> Create Vertical Chain. Change the text attribute of the top button so that it reads “Consume Purchase” and the id to consumeButton. Also, configure the onClick property to call a method named consumePurchase.
Select the bottom-most button and repeat the above steps, setting the text to “Buy Product”, the id to buyButton, and the onClick callback to makePurchase. Once completed, the layout should match that shown in Figure 88-1:
Figure 88-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 is 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 appears. Within this section, select the In-app products option listed under Products, as shown in Figure 88-2:
Figure 88-2
On the In-app products page, click on the Create product button:
Figure 88-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 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 88-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 88-5
Now that the app and the in-app product have been set up in the Play Console, we can add code to the project.
Initializing the Billing Client
Edit the MainActivity.kt file and make the following changes to begin implementing the in-app purchase functionality:
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, queryProduct() is called. This method and the purchasesUpdatedListener assigned to the billing client must 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.kt file, add the queryProduct() method so that it reads as follows:
Much of the code used here should be familiar from the previous chapter. The listener code checks that at least one product 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 is displayed on the TextView. One point of note is that when we 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 it cannot safely make direct changes to the user interface. The runOnUiThread() method provides a quick and convenient way to execute code on the main thread without using coroutines.
Launching the Purchase Flow
When the user clicks the buy button, makePurchase() will be called to start the purchase process. We can now add this method as follows:
The results of the purchase process will be reported to the app via the PurchaseUpdatedListener assigned to the billing client during the initialization phase. Add this handler now as follows:
The handler will output log messages if the user cancels the purchase or another error occurs. However, a successful purchase results in a call to the completePurchase() method, which is passed the current Purchase object. Add this method as outlined below:
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 enabled, and the user is notified of the successful purchase. The buy button is also disabled to prevent the user from repurchasing before consuming the purchase.
Consuming the Product
With the user now able to click on the “consume” button, the next step is to ensure 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:
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). This is passed to the consumePurchase() method, which is launched within a coroutine using the IO dispatcher. If the product is successfully consumed, code is executed in the main thread to disable the consume button, enable the buy button, and update the status text.
Restoring a Previous Purchase
With the code added so far, we can purchase and consume a product within a single session. If we were to make a purchase and exit the app before consuming it, the purchase would be lost when the app restarts. We can solve this problem by configuring a QueryPurchasesParams instance to search for the unconsumed In-App product and passing it to the queryPurchasesAsync() method of the billing client together with a reference to a listener that will be called with the results. Add a new function and the listener to the MainActivity.kt file as follows:
If the list of purchases passed to the listener is not empty, the first purchase in the list is assigned to the purchase variable, and the consume button is enabled (in a more complete implementation, code should be added to check this is the correct product by comparing the product id and to handle the return of multiple purchases). If no purchases are found, the consume button is disabled until another purchase is made. All that remains is to call our new reloadPurchase() method during the billing setup process as follows:
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 need to increase the version number in the build.gradle.kts (Module: app) file:
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. Next, using the internal testing link, install the app on a device or emulator where one of the test accounts is signed in. To locate the testing link, select the app in the Google Play Console and choose the Internal testing option from the navigation panel followed by the Testers tab, as shown in Figure 88-6:
Figure 88-6
Scroll to the “How testers join your test” section of the screen and click on Copy link:
Figure 88-7
Open the Chrome browser on the testing device or emulator, enter the testing link, and follow the instructions to install the app from the Play Store. After the app starts, it should, after a short delay, display the product name on the TextView. Clicking the buy button will begin the purchase flow, as shown in Figure 88-8:
Figure 88-8
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 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. Make another purchase, then terminate and restart the app. The app should locate the previous unconsumed purchase and enable the consume button.
Troubleshooting
If you encounter problems with the purchase, make sure the device is attached to Android Studio 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:
Note that as long as you leave the app version number unchanged in the module-level build.gradle.kts 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, ensure the device user account has been added to the license testers list. If the app is running on a physical device, try 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 demonstrating adding an in-app product to an Android app. This included the creation of the product within the Google Play Console and writing code to initialize and connect to the billing client, querying available products, and purchasing and consuming the product. We also explained how to add license testers using the Play Console to make purchases during testing without spending money.
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 how to integrate 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 Bundlechapter. 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:
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. When working with Kotlin, the Google Play Kotlin Extensions Library is also recommended:
Once the build file has been modified 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 1-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 val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode ==
BillingClient.BillingResponseCode.OK
&& purchases != null
) {
for (purchase in purchases) {
// Process the purchases
}
} elseif (billingResult.responseCode ==
BillingClient.BillingResponseCode.USER_CANCELED
) {
// Purchase cancelled by user
} else {
// Handle errors here
}
}
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()Code language:JavaScript(javascript)
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 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(object : BillingClientStateListener {
override fun onBillingSetupFinished(
billingResult: BillingResult
) {
if (billingResult.responseCode ==
BillingClient.BillingResponseCode.OK
) {
// Connection successful
} else {
// Connection failed
}
}
override fun onBillingServiceDisconnected() {
// Connection to billing service lost
}
})Code language:JavaScript(javascript)
Querying 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):
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(
ImmutableList.of(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(
BillingClient.ProductType.INAPP
)
.build()
)
)
.build()
billingClient.queryProductDetailsAsync(
queryProductDetailsParams
) { billingResult, productDetailsList ->
if (!productDetailsList.isEmpty()) {
// Process list of matching products
} else {
// No product matches found
}
}Code language:JavaScript(javascript)
The queryProductDetailsAsync() method is passed a ProductDetailsResponseListener handler (in this case in the form of a lambda code block) 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.
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.
} elseif (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
// Payment is still pending
}Code language:JavaScript(javascript)
Note that your app will only support pending purchases if a call is made to the 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:
billingClient.acknowledgePurchase(acknowledgePurchaseParams,
acknowledgePurchaseResponseListener);
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
val acknowledgePurchaseResponseListener = AcknowledgePurchaseResponseListener {
// Check acknowledgement result
}
billingClient.acknowledgePurchase(
acknowledgePurchaseParams,
acknowledgePurchaseResponseListener
)Code language:JavaScript(javascript)
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 and a call to the billing client’s consumePurchase() method:
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
coroutineScope.launch {
val result = billingClient.consumePurchase(consumeParams)
if (result.billingResult.responseCode ==
BillingClient.BillingResponseCode.OK) {
// Purchase successfully consumed
}
}Code language:JavaScript(javascript)
Querying Previous Purchases
When working with in-app billing it is a common requirement 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 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 items that have not yet been consumed:
val queryPurchasesParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchasesAsync(
queryPurchasesParams,
purchasesListener
)
.
.
private val purchasesListener =
PurchasesResponseListener { billingResult, purchases ->
if (!purchases.isEmpty()) {
// Access existing active purchases
} else {
// No
}
}Code language:PHP(php)
To obtain a list of active subscriptions, change the ProductType value from INAPP to SUBS.
Alternatively, to obtain a list of the most recent purchases for each product, make a call to the BillingClient queryPurchaseHistoryAsync() method:
val queryPurchaseHistoryParams = QueryPurchaseHistoryParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchaseHistoryAsync(queryPurchaseHistoryParams) { billingResult, historyList ->
// Process purchase history list
}Code language:JavaScript(javascript)
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.
Touch sensors are now built into many Android devices to identify the user and provide access to the device and application functionality, such as in-app payment options using fingerprint recognition. Fingerprint recognition is just one of several authentication methods, including passwords, PINs, and, more recently, facial recognition.
Although only a few Android devices currently on the market provide facial recognition, this will likely become more common in the future. In recognition of this, Google has begun to transition away from a fingerprint-centric approach to adding authentication to apps to a less specific approach called biometric authentication.
This chapter provides an overview of biometric authentication and a detailed, step-by-step tutorial demonstrating a practical approach to implementing biometric authentication within an Android app project.
An Overview of Biometric Authentication
The key biometric authentication component is the BiometricPrompt class. This class performs much of the work that previously had to be performed by writing code in earlier Android versions, including displaying a standard dialog to guide the user through the authentication process, performing the authentication, and reporting the results to the app. The class also handles excessive failed authentication attempts and enforces a timeout before the user can try again.
The BiometricPrompt class includes a companion Builder class that can be used to configure and create BiometricPrompt instances, including defining the text that is to appear within the biometric authentication dialog and the customization of the cancel button (also referred to as the negative button) that appears in the dialog.
The BiometricPrompt instance is also assigned a set of authentication callbacks that will be called to provide the app with the results of an authentication operation. A CancellationSignal instance is also used to allow the app to cancel the authentication while it is in process.
With these basics covered, the remainder of this chapter will implement fingerprint-based biometric authentication within an example project.
Creating the Biometric Authentication Project
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.
Enter BiometricDemo into the Name field and specify com.ebookfrenzy.biometricdemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 29: Android (Q) and the Language menu to Kotlin.
Configuring Device Fingerprint Authentication
Fingerprint authentication is only available on devices containing a touch sensor and on which the appropriate configuration steps have been taken to secure the device and enroll at least one fingerprint. For steps on configuring an emulator session to test fingerprint authentication, refer to the chapter Using and Configuring the Android Studio AVD Emulator.
Configure fingerprint authentication on a physical device by opening the Settings app and selecting the Security option. Within the Security settings screen, select the Fingerprint option. Tap the Next button on the resulting information screen to proceed to the Fingerprint setup screen. Before fingerprint security can be enabled, a backup screen unlocking method (such as a PIN) must be configured. If the lock screen is not secured, follow the steps to configure PIN, pattern, or password security.
With the lock screen secured, proceed to the fingerprint detection screen and touch the sensor when prompted lock screen secured, proceed to the fingerprint detection screen and touch the sensor when prompted (Figure 85-1), repeating the process to add additional fingerprints if required.
Figure 85-1
Adding the Biometric Permission to the Manifest File
Biometric authentication requires that the app request the USE_BIOMETRIC permission within the project manifest file. Within the Android Studio Project tool window, locate and edit the app -> manifests -> AndroidManifest.xml file to add the permission request as follows:
To keep the example as simple as possible, the only visual element within the user interface will be a Button view. Locate and select the activity_main.xml layout resource file to load it into the Layout Editor tool.
Delete the sample TextView object, drag and drop a Button object from the Common category of the palette and position it in the center of the layout canvas. Using the Attributes tool window, change the text property on the button to “Authenticate” and extract the string to a resource. Finally, configure the onClick property to call a method named authenticateUser.
On completion of the above steps, the layout should match that shown in Figure 85-2:
Figure 85-2
Adding a Toast Convenience Method
At various points throughout the code in this example, the app will be designed to display information to the user via Toast messages. Rather than repeat the same Toast code multiple times, a convenience method named notifyUser() will be added to the main activity. This method will accept a single String value and display it to the user as a Toast message. Edit the MainActivity.kt file now and add this method as follows:
Earlier in this chapter, steps were taken to configure the lock screen and register fingerprints on the device or emulator on which the app will be tested. It is important, however, to include defensive code in the app to ensure these requirements have been met before attempting to seek fingerprint authentication. These steps will be performed within the onCreate method residing in the MainActivity.kt file, using the Keyguard and PackageManager manager services. Note that code has also been added to verify that the USE_BIOMETRIC permission has been configured for the app:
The above code changes begin by using the Keyguard manager to verify that a backup screen unlocking method has been configured (in other words, a PIN or other authentication method can be used as an alternative to fingerprint authentication to unlock the screen). If the lock screen is not secured, the code reports the problem to the user and returns from the method.
The method then checks that the user has biometric authentication permission enabled for the app before using the package manager to verify that fingerprint authentication is available on the device.
Configuring the Authentication Callbacks
When the biometric prompt dialog is configured, it will need to be assigned a set of authentication callback methods that can be called to notify the app of the success or failure of the authentication process. These methods need to be wrapped in a BiometricPrompt.AuthenticationCallback class instance. Remaining in the MainActivity.kt file, add a method to create and return an instance of this class with the appropriate methods implemented:
Once initiated, the biometric authentication process is performed independently of the app. To provide the app with a way to cancel the operation, an instance of the CancellationSignal class is created and passed to the biometric authentication process. This CancellationSignal instance can then be used to cancel the process if necessary. The cancellation signal instance may be configured with a listener, which will be called when the cancellation is completed. Add a new method to the activity class to configure and return a CancellationSignal object as follows:
All that remains is to add code to the authenticateUser() method to create and configure a BiometricPrompt instance and initiate the authentication. Add the authenticateUser() method as follows:
.
.
import android.view.View
.
.
funauthenticateUser(view: View) {
val biometricPrompt = BiometricPrompt.Builder(this)
.setTitle("Biometric Demo")
.setSubtitle("Authentication is required to continue")
.setDescription("This app uses biometric authentication to protect your data.")
.setNegativeButton("Cancel", this.mainExecutor)
{ _, _ ->
notifyUser("Authentication cancelled") }.build()
biometricPrompt.authenticate(getCancellationSignal(), mainExecutor,
authenticationCallback)
}Code language:Kotlin(kotlin)
The BiometricPrompt.Builder class creates a new BiometricPrompt instance configured with title, subtitle, and description text to appear in the prompt dialog. The negative button is configured to display text which reads “Cancel” and a listener is configured to display a message when this button is clicked. Finally, the authenticate() method of the BiometricPrompt instance is called and passed the AuthenticationCallback and CancellationSignal instances. The Biometric prompt also needs to know which thread to perform the authentication on. This is defined by passing through an Executor object configured for the required thread. In this case, the getMainExecutor() method is used to pass a main Executor object to the BiometricPrompt instance so that the authentication process occurs on the app’s main thread.
Testing the Project
With the project now complete, run the app on a physical Android device or emulator session and click on the Authenticate button to display the BiometricPrompt dialog as shown in Figure 85-3:
Figure 85-3
Once running, either touch the fingerprint sensor or use the extended controls panel within the emulator to simulate a fingerprint touch as outlined in the chapter entitled Using and Configuring the Android Studio AVD Emulator. Assuming a registered fingerprint is detected, the prompt dialog will return to the main activity, where the toast message from the successful authentication callback method will appear.
Click the Authenticate button again, using an unregistered fingerprint to attempt the authentication. This time the biometric prompt dialog will indicate that the fingerprint was not recognized:
Figure 85-4
Verify that the error handling callback works by clicking on the activity outside the biometric prompt dialog. The prompt dialog will disappear, and the toast message will appear with the following message:
Authentication error: Fingerprint operation cancelled by user.Code language:plaintext(plaintext)
Check that canceling the prompt dialog using the Cancel button triggers the “Authentication Canceled” toast message. Finally, attempt to authenticate multiple times using an unregistered fingerprint and note that after several attempts, the prompt dialog indicates that too many failures have occurred and that future attempts cannot be made until later.
Summary
This chapter has outlined how to integrate biometric authentication into an Android app project. This involves using the BiometricPrompt class, which automatically handles most of the authentication process once configured with appropriate message text and callbacks.
This chapter aims to provide a practical demonstration of both Android app links and the Android Studio App Link Assistant.
This chapter will add app linking support to an existing Android app, allowing an activity to be launched via an app link URL. In addition to launching the activity, the content displayed will be specified within the URL’s path.
About the Example App
The project used in this chapter is named AppLinking and is a basic app designed to allow users to find information about landmarks in London. The app uses a SQLite database accessed through a standard Android content provider class. The app has an existing database containing records for some popular tourist attractions in London. In addition to the existing database entries, the app lets the user add and delete landmark descriptions.
Currently, the app allows the existing records to be searched and new records to be added and deleted.
The project consists of two activities named AppLinkingActivity and LandmarkActivity. AppLinkingActivity is the main activity launched at app startup. This activity allows the user to enter search criteria and add records to the database. When a search locates a matching record, LandmarkActivity launches and displays the information for the related landmark.
This chapter will enhance the app to support app linking so that URLs can be used to display specific landmark records within the app.
The Database Schema
The data for the example app is contained within a file named landmarks.db located in the app -> assets –> databases folder of the project hierarchy. The database contains a single table named locations, the structure of which is outlined in Table 84-1:
Column
Type
Description
_id
String
The primary index, this column contains string values that uniquely identify the landmarks in the database.
title
String
The name of the landmark (e.g., London Bridge).
description
String
A description of the landmark.
personal
Boolean
Indicates whether the record is personal or public. This value is set to true for all records added by the user. Existing records provided with the database are set to false.
Table 84-1
Loading and Running the Project
The project is contained within the AppLinking folder of the sample source code download archive located at the following URL:
Having located the folder, open it within Android Studio and run the app on a device or emulator. Once the app is launched, the screen illustrated in Figure 84-1 below will appear:
Figure 84-1
As currently implemented, landmarks are located using the ID for the location. The default database configuration currently contains two records referenced by the IDs “londonbridge” and “toweroflondon”. Test the search feature by entering londonbridge into the ID field and clicking the Find button. When a matching record is found, the second activity (LandmarkActivity) is launched and passed information about the record to be displayed. This information takes the form of extra data added to the Intent object. LandmarkActivity uses this information to extract the record from the database and display it to the user using the screen shown in Figure 84-2.
Figure 84-2
Adding the URL Mapping
Now that the app has been loaded into Android Studio and tested, we can add app link support. The objective is for the LandmarkActivity screen to launch and display information in response to an app link click. This is achieved by mapping a URL to LandmarkActivity. For this example, the format of the URL will be as follows:
To add a URL mapping to the project, begin by opening the App Links Assistant using the Tools -> App Links Assistant menu option. Once open, the assistant should appear as shown in Figure 84-3:
Figure 84-3
Click on the Open URL Mapping Editor button to map a URL to an activity. Within the mapping screen, click on the ‘+’ button (highlighted in Figure 84-4) to add a new URL:
Figure 84-4
In the Host field of the Add URL Mapping dialog, enter either the URL for your own website. If you do not have a website to use for this tutorial, you can still follow most of this chapter using http://www.example.com, though it will not be possible to test features that require the presence of a Digital Asset Links file.
The Path field (marked B in Figure 84-5 below) is where the path component of the URL is declared. The path must be prefixed with / so enter /landmarks into this field.
The Path menu (B) provides the following three path-matching options:
path – The URL must match the path component of the URL exactly to launch the activity. For example, if the path is set to /landmarks, http://www.example.com/landmarks will be considered a match. A URL of http:// www.example.com/landmarks/londonbridge, however, will not be considered a match.
pathPrefix – The specified path is only considered as the prefix. Additional path components may be included after the /landmarks component (for example, http://www.example.com/landmarks/londonbridge will still be considered a match).
pathPattern – Allows the path to be specified using pattern matching in the form of basic regular expressions and wildcards, for example, landmarks/*/[l-L]ondon/*
Since the path in this example is a prefix to the landmark ID component, select the pathPrefix menu option.
Finally, use the Activity menu (C) to select LandmarkActivity as the activity to be launched in response to the app link:
Figure 84-5
After completing the settings in the dialog, click the OK button to commit the changes. Check that the URL is correctly formatted and assigned to the appropriate activity by entering the following URL into the Check URL Mapping field of the mapping screen (where <your domain> is set to the domain specified in the Host field above) :
If the mapping is configured correctly, LandmarkActivity will be listed as the mapped activity:
Figure 84-6
The latest version of Android requires that App Links be declared for HTTP and HTTPS protocols, even if only one is being used. Therefore, before proceeding to the next step, repeat the above steps to add the HTTPS version of the URL to the list.
The next step will also be performed in the URL mapping screen of the App Links Assistant, so leave the screen selected.
Adding the Intent Filter
As explained in the previous chapter, an intent filter is needed to launch the target activity in response to an app link click. In fact, when the URL mapping was added, the intent filter was automatically added to the project manifest file. With the URL mapping selected in the App Links Assistant URL mapping list, scroll down the screen until the intent filter Preview section comes into view. The preview should contain the modified AndroidManifest.xml file with the newly added intent filters included:
Figure 84-7
Although App Links Assistant has added intent filters for us, it may not have included the autoVerify setting needed when working with app links. Open the manifests -> AndroidManifest.xml file and add this setting to the two intent filters as follows:
The steps taken so far ensure that the correct activity is launched in response to an appropriately formatted app link URL. The next step is to handle the intent within the LandmarkActivity class so that the correct record is extracted from the database and displayed to the user. Before making any changes to the code within the LandmarkActivity.kt file, it is worthwhile reviewing some areas of the existing code. Open the LandmarkActivity.kt file in the code editor and locate the onCreate() and handleIntent() methods which should currently read as follows:
In its current form, the code expects to find the landmark ID within the extra data of the Intent bundle. Since the activity can now be launched by an app link, this code must be changed to handle both scenarios. Begin by deleting the call to handleIntent() in the onCreate() method:
To add the initial app link intent handling code, return to the App Links Assistant panel and click on the Select Activity button listed under step 2. Within the activity selection dialog, select the LandmarkActivity entry before clicking on the Insert Code button:
Figure 84-8
Return to the LandmarkActivity.kt file and note that the following code has been inserted into the onCreate() method (note that you can manually add this code if Android Studio is unable to complete the request):
// ATTENTION: This was auto-generated to handle app links.val appLinkIntent: Intent = intent
val appLinkAction: String? = appLinkIntent.action
val appLinkData: Uri? = appLinkIntent.dataCode language:Kotlin(kotlin)
This code accesses the Intent object and extracts the Action string and Uri. If the activity launch results from an app link, the action string will be set to android.intent.action.VIEW, which matches the action declared in the intent filter added to the manifest file. If, on the other hand, the activity was launched by the standard intent launching code in the findLandmark() method of the main activity, the action string will be null. By checking the value assigned to the action string, code can be written to identify how the activity was launched and take appropriate action:
overridefunonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLandmarkBinding.inflate(layoutInflater)
setContentView(binding.root)
// ATTENTION: This was auto-generated to handle app links.val appLinkIntent: Intent = intent
val appLinkAction: String? = appLinkIntent.action
val appLinkData: Uri? = appLinkIntent.dataval landmarkId = appLinkData?.lastPathSegment
if (landmarkId != null) {
displayLandmark(landmarkId)
}
}Code language:Kotlin(kotlin)
All that remains is to add some additional code to the method to identify the last component in the app link URL path and to use that as the landmark ID when querying the database:
overridefunonCreate(savedInstanceState: Bundle?) {
.
.
// ATTENTION: This was auto-generated to handle app links.val appLinkIntent = intent
val appLinkAction = appLinkIntent.action
val appLinkData = appLinkIntent.dataif (appLinkAction != null) {
if (appLinkAction == "android.intent.action.VIEW") {
val landmarkId = appLinkData?.lastPathSegment
if (landmarkId != null) {
displayLandmark(landmarkId)
}
}
} else {
handleIntent(appLinkIntent)
}
}Code language:Kotlin(kotlin)
If the action string is not null, a check is made to verify that it is set to android.intent.action.VIEW before extracting the last component of the Uri path. This component is then used as the landmark ID when making the database query. If, on the other hand, the action string is null, the existing handleIntent() method is called to extract the ID from the intent data.
Testing the App
Run the app on a device or emulator and make sure it is still possible to search for the example landmarks. We have now successfully added app link support to the app. If you specified your own website URL for the app links, we can now take the example one step further by creating and installing a Digital Asset Links file.
Creating the Digital Asset Links File
As outlined in the chapter entitled An Android Intents Overview, to fully support app links, we need to install a Digital Asset Links file on the website referenced in the app link. Begin by following the steps outlined in An Android Intents Overview to locate your debug.keystore file and identify your SHA256 fingerprint.
Once the page has loaded, enter your website URL into the Hosting site domain field, com.ebookfrenzy.applinking as the App package name, and your SHA256 fingerprint into the App package fingerprint (SHA256) field:
Figure 84-9
Click the Generate statement button to display the generated statement and place it in a file named assetlinks.json in a folder named .well-known on your web server. Return to the generator page and click on the Test statement button to verify that the file is in the correct location. On a successful test, output similar to the following will appear:
Figure 84-10
Assuming a successful test, we are now ready to try out the app link.
Testing the App Link
Test that the intent handling works by returning to the App Links Assistant panel and clicking on the Test App Links button. When prompted for a URL to test, enter the URL (using the domain referenced in the app link mapping) for the londonbridge landmark ID before clicking on the Run Test button:
Figure 84-11
Once the button has been clicked, the Landmark activity should launch on the device or emulator and display information about London Bridge.
Summary
This chapter has demonstrated the steps for implementing App Link support within an Android app project, including using the App Link Assistant in Android Studio, App Link URL mapping, intent filters, handling website association using Digital Asset Links file entries, and App Link testing.
As technology evolves, the traditional distinction between web and mobile content is beginning to blur. One area where this is particularly true is the growing popularity of progressive web apps, where web apps look and behave much like traditional mobile apps.
Another trend involves making the content within mobile apps discoverable through web searches and via URL links. In the context of Android app development, the App Links feature is designed to make it easier for users to discover and access content stored within an Android app, even if the user does not have the app installed.
An Overview of Android App Links
An app link is a standard HTTP URL that is an easy way to link directly to a particular place in your app from an external source such as a website or app. App links (also called deep links) are used primarily to encourage users to engage with an app and to allow users to share app content.
App link implementation is a multi-step process that involves the addition of intent filters to the project manifest, implementing link handling code within the associated app activities, and the use of digital asset links files to associate app and web-based content.
These steps can be performed manually by making changes within the project or automatically using the Android Studio App Links Assistant.
These steps can be performed manually by making project changes or automatically using the Android Studio App Links Assistant.
The remainder of this chapter will outline app links implementation in terms of the changes that must be made to a project. The next chapter (An Android Studio App Links Tutorial) will demonstrate the use of the App Links Assistant to achieve the same results.
App Link Intent Filters
An app link URL needs to be mapped to a specific activity within an app project. This is achieved by adding intent filters to the project’s AndroidManifest.xml file designed to launch an activity in response to an android.intent.action.VIEW action. The intent filters are declared within the element for the activity to be launched and must contain the data outlining the scheme, host, and path of the app link URL. The following manifest fragment, for example, declares an intent filter to launch an activity named MyActivity when an app link matching http:// www.example.com/welcome is detected:
The intent filter will cause the app link to launch the correct activity, but code must still be added to the target activity to handle the intent appropriately.
Handling App Link Intents
In most cases, the launched activity will need to gain access to the app link URL and take specific action based on how the URL is structured. Continuing from the above example, the activity will likely display different content when launched via a URL containing a path of /welcome/newuser than one with the path set to /welcome/existinguser.
When the link launches the activity, it is passed an intent object containing data about the action which launched the activity, including a Uri object containing the app link URL. Within the initialization stages of the activity, code can be added to extract this data as follows:
val appLinkIntent = intent
val appLinkAction = appLinkIntent.action
val appLinkData = appLinkIntent.dataCode language:Kotlin(kotlin)
Having obtained the Uri for the app link, the various components that make up the URL path can be used to decide the actions to perform within the activity. In the following code example, the last component of the URL is used to identify whether content should be displayed for a new or existing user:
val userType = appLinkData.lastPathSegment
if (userType == "newuser") {
// display new user content
} else {
// display existing user content
}Code language:Kotlin(kotlin)
Associating the App with a Website
Before an app link will work, an app link URL must be associated with the website on which the app link is based. This is achieved by creating a Digital Asset Links file named assetlinks.json and installing it within the website’s .well-known folder. Note that digital asset linking is only possible for websites that are HTTPS based.
A digital asset links file comprises a relation statement granting permission for a target app to be launched using the website’s link URLs and a target statement declaring the companion app package name and SHA-256 certificate fingerprint for that project. A typical asset link file might, for example, read as follows:
The assetlinks.json file can contain multiple digital asset links, allowing a single website to be associated with more than one companion app.
Summary
Android App Links allow app activities to be launched via URL links from external websites and other apps. App links are implemented using intent filters within the project manifest file and intent handling code within the launched activity. Using a Digital Asset Links file, it is also possible to associate the domain name used in an app link with the corresponding website. Once the association has been established, Android no longer needs to ask the user to select the target app when an app link is used.
As we have seen in the preceding chapters, the Android Printing framework makes it relatively easy to build printing support into applications as long as the content is in the form of an image or HTML markup. More advanced printing requirements can be met by using the custom document printing feature of the Printing framework.
An Overview of Android Custom Document Printing
In simplistic terms, custom document printing uses canvases to represent the pages of the document to be printed. The application draws the content to be printed onto these canvases as shapes, colors, text, and images. The canvases are represented by instances of the Android Canvas class, providing access to a rich selection of drawing options. Once all the pages have been drawn, the document is then printed.
While this sounds simple enough, some steps need to be performed to make this happen, which can be summarized as follows:
Implement a custom print adapter sub-classed from the PrintDocumentAdapter class.
Obtain a reference to the Print Manager Service.
Create an instance of the PdfDocument class to store the document pages.
Add pages to the PdfDocument in the form of PdfDocument.Page instances.
Obtain references to the Canvas objects associated with the document pages.
Draw content onto the canvases.
Write the PDF document to a destination output stream provided by the Printing framework.
Notify the Printing framework that the document is ready to print.
This chapter will provide an overview of these steps, followed by a detailed tutorial designed to demonstrate the implementation of custom document printing within Android applications.
Custom Print Adapters
The role of the print adapter is to provide the Printing framework with the content to be printed and to ensure that it is formatted correctly for the user’s chosen preferences (considering factors such as paper size and page orientation).
Much of this work is performed by the print adapters provided as part of the Android Printing framework and designed for these specific printing tasks when printing HTML and images. When printing a web page, for example, a print adapter is created for us when a call is made to the createPrintDocumentAdapter() method of an instance of the WebView class.
In the case of custom document printing, however, it is the responsibility of the application developer to design the print adapter and implement the code to draw and format the content in preparation for printing.
Custom print adapters are created by sub-classing the PrintDocumentAdapter class and overriding a set of callback methods within that class which will be called by the Printing framework at various stages in the print process. These callback methods can be summarized as follows:
· onStart() – This method is called when the printing process begins and is provided so that the application code can perform any necessary tasks to create the print job. Implementation of this method within the PrintDocumentAdapter sub-class is optional.
· onLayout() – This callback method is called after the call to the onStart() method and then again each time the user makes changes to the print settings (such as changing the orientation, paper size, or color settings). This method should adapt the content and layout to accommodate these changes. Once these changes are completed, the method must return the number of pages to be printed. Implementation of the onLayout() method within the PrintDocumentAdapter sub-class is mandatory.
· onWrite() – This method is called after each call to onLayout() and is responsible for rendering the content on the canvases of the pages to be printed. Amongst other arguments, this method is passed a file descriptor to which the resulting PDF document must be written once rendering is complete. A call is then made to the onWriteFinished() callback method passing through an argument containing information about the page ranges to be printed. Implementation of the onWrite() method within the PrintDocumentAdapter sub-class is mandatory.
· onFinish() – An optional method which, if implemented, is called once by the Printing framework when the printing process is completed, thereby providing the application the opportunity to perform any clean-up operations that may be necessary.
Preparing the Custom Document Printing Project
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.
Enter CustomPrint into the Name field and specify com.ebookfrenzy.customprint as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.
Load the activity_main.xml layout file into the Layout Editor tool and, in Design mode, select and delete the “Hello World!” TextView object. Drag and drop a Button view from the Common section of the palette and position it in the center of the layout view. With the Button view selected, change the text property to “Print Document” and extract the string to a new resource. On completion, the user interface layout should match that shown in Figure 82-1:
Figure 82-1
When the button is selected within the application, it will be required to call a method to initiate the document printing process. Remaining within the Attributes tool window, set the onClick property to call a method named printDocument.
Creating the Custom Print Adapter
Most of the work involved in printing a custom document from within an Android application involves the implementation of the custom print adapter. This example will require a print adapter with the onLayout() and onWrite() callback methods implemented. Within the MainActivity.kt file, add the template for this new class so that it reads as follows:
As the new class currently stands, it contains a constructor method to be called when a new class instance is created. The constructor takes the context of the calling activity as an argument, which is then stored so that it can be referenced later in the two callback methods.
With the class outline established, the next step is implementing the two callback methods, beginning with onLayout().
Implementing the onLayout() Callback Method
Remaining within the MainActivity.kt file, begin by adding some import directives that will be required by the code in the onLayout() method:
Note that for this example, a four-page document will be printed. In more complex situations, the application will most likely need to dynamically calculate the number of pages to be printed based on the quantity and layout of the content in relation to the user’s paper size and page orientation selections.
With the variables declared, implement the onLayout() method as outlined in the following code listing:
overridefunonLayout(oldAttributes: PrintAttributes?,
newAttributes: PrintAttributes?,
cancellationSignal: CancellationSignal?,
callback: LayoutResultCallback?,
metadata: Bundle?) {
myPdfDocument = PrintedPdfDocument(context, newAttributes)
val height = newAttributes.mediaSize?.heightMils
val width = newAttributes.mediaSize?.heightMils
height?.let {
pageHeight = it / 1000 * 72
}
width?.let {
pageWidth = it / 1000 * 72
}
cancellationSignal?.let {
if (it.isCanceled) {
callback?.onLayoutCancelled()
return
}
}
if (totalpages > 0) {
val builder =
PrintDocumentInfo.Builder("print_output.pdf").setContentType(
PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(totalpages)
val info = builder.build()
callback?.onLayoutFinished(info, true)
} else {
callback?.onLayoutFailed("Page count is zero.")
}
}Code language:Kotlin(kotlin)
This method performs quite a few tasks, each requiring some detailed explanation.
To begin with, a new PDF document is created as a PdfDocument class instance. One of the arguments passed into the onLayout() method when the Printing framework calls it is an object of type PrintAttributes containing details about the paper size, resolution, and color settings the user selects for the print output. These settings are used when creating the PDF document, along with the context of the activity previously stored for us by our constructor method:
The method then uses the PrintAttributes object to extract the height and width values for the document pages. These dimensions are stored in the object as thousandths of an inch. Since the methods that will use these values later in this example work in units of 1/72 of an inch, these numbers are converted before they are stored:
val height = newAttributes?.mediaSize?.heightMils
val width = newAttributes?.mediaSize?.heightMils
height?.let {
pageHeight = it / 1000 * 72
}
width?.let {
pageWidth = it / 1000 * 72
}Code language:Kotlin(kotlin)
Although this example does not make use of the user’s color selection, this property can be obtained via a call to the getColorMode() method of the PrintAttributes object, which will return a value of either COLOR_MODE_COLOR or COLOR_MODE_MONOCHROME.
When the onLayout() method is called, it is passed an object of type LayoutResultCallback. This object provides a way for the method to communicate status information back to the Printing framework via a set of methods. For example, the onLayout() method will be called if the user cancels the print process. The fact that the process has been canceled is indicated via a setting within the CancellationSignal argument. If a cancellation is detected, the onLayout() method must call the onLayoutCancelled() method of the LayoutResultCallback object to notify the Print framework that the cancellation request was received and that the layout task has been canceled:
cancellationSignal?.let {
if (it.isCanceled) {
callback?.onLayoutCancelled()
return
}
}Code language:Kotlin(kotlin)
When the layout work is complete, the method is required to call the onLayoutFinished() method of the LayoutResultCallback object, passing through two arguments. The first argument is a PrintDocumentInfo object containing information about the document to be printed. This information consists of the name of the PDF document, the type of content (in this case, a document rather than an image), and the page count. The second argument is a Boolean value indicating whether or not the layout has changed since the last call made to the onLayout() method:
if (totalpages > 0) {
val builder = PrintDocumentInfo.Builder("print_output.pdf").setContentType(
PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(totalpages)
val info = builder.build()
callback?.onLayoutFinished(info, true)
} else {
callback?.onLayoutFailed("Page count is zero.")
}Code language:Kotlin(kotlin)
If the page count is zero, the code reports this failure to the Printing framework via a call to the onLayoutFailed() method of the LayoutResultCallback object.
The call to the onLayoutFinished() method notifies the Printing framework that the layout work is complete, triggering a call to the onWrite() method.
Implementing the onWrite() Callback Method
The onWrite() callback method is responsible for rendering the pages of the document and then notifying the Printing framework that the document is ready to be printed. When completed, the onWrite() method reads as follows:
The onWrite() method starts by looping through each page in the document. However, it is important to consider that the user may have requested that only some of the pages that make up the document be printed. The Printing framework user interface panel provides the option to specify specific pages or ranges of pages to be printed. Figure 82-2, for example, shows the print panel configured to print pages 1-4, page 9, and pages 1113 of a document.
Figure 82-2
When writing the pages to the PDF document, the onWrite() method must take steps to ensure that only those pages specified by the user are printed. To make this possible, the Printing framework passes through as an argument an array of PageRange objects indicating the ranges of pages to be printed. In the above onWrite() implementation, the pageInRange() method is called for each page to verify that the page is within the specified ranges. The code for the pageInRange() method will be implemented later in this chapter.
for (i in0 until totalpages) {
if (pageInRange(pageRanges, i)) {Code language:Kotlin(kotlin)
For each page that is within any specified ranges, a new PdfDocument.Page object is created. When creating a new page, the height and width values previously stored by the onLayout() method are passed through as arguments so that the page size matches the print options selected by the user:
val newPage = PageInfo.Builder(pageWidth, pageHeight, i).create()
val page = myPdfDocument?.startPage(newPage)Code language:Kotlin(kotlin)
As with the onLayout() method, the onWrite() method is required to respond to cancellation requests. In this case, the code notifies the Printing framework that the cancellation has been performed before closing and dereferencing the myPdfDocument variable:
As long as the print process has not been canceled, the method calls a method to draw the content on the current page before calling the finishedPage() method on the myPdfDocument object.
The drawPage() method is responsible for drawing the content onto the page and will be implemented once the onWrite() method is complete.
When the required number of pages have been added to the PDF document, the document is then written to the destination stream using the file descriptor, which is passed through as an argument to the onWrite() method. If, for any reason, the write operation fails, the method notifies the framework by calling the onWriteFailed() method of the WriteResultCallback object (also passed as an argument to the onWrite() method).
Finally, the onWriteFinish() method of the WriteResultsCallback object is called to notify the Printing framework that the document is ready to be printed.
Checking a Page is in Range
As previously outlined, when the onWrite() method is called, it is passed an array of PageRange objects indicating the ranges of pages within the document to be printed. The PageRange class is designed to store the start and end pages of a page range which, in turn, may be accessed via the getStart() and getEnd() methods of the class.
When the onWrite() method was implemented in the previous section, a call was made to a method named pageInRange(), which takes as arguments an array of PageRange objects and a page number. The role of the pageInRange() method is to identify whether the specified page number is within the ranges specified and may be implemented within the MyPrintDocumentAdapter class in the MainActivity.kt class as follows:
innerclassMyPrintDocumentAdapter(privatevar context: Context) :
PrintDocumentAdapter() {
.
.
privatefunpageInRange(pageRanges: Array<outPageRange>?, page: Int):
Boolean {
pageRanges?.let {
for (i in it.indices) {
if (page >= it[i].start && page <= it[i].end)
returntrue
}
}
returnfalse
}
.
.
}Code language:Kotlin(kotlin)
Drawing the Content on the Page Canvas
We have now reached the point where some code needs to be written to draw the content on the pages so that they are ready for printing. The content that gets drawn is completely application specific and limited only by what can be achieved using the Android Canvas class. In this example, however, some simple text and graphics will be drawn on the canvas.
The onWrite() method has been designed to call a method named drawPage() which takes as arguments the PdfDocument.Page object representing the current page, and an integer, representing the page number. Within the MainActivity.kt file, this method should now be implemented as follows:
Page numbering within the code starts at 0. Since documents traditionally start at page 1, the method begins by incrementing the stored page number. A reference to the Canvas object associated with the page is then obtained, and some margin and baseline values are declared:
var pagenum = pagenumber
val canvas = page.canvas
pagenum++ // Make sure page numbers start at 1val titleBaseLine = 72val leftMargin = 54Code language:Kotlin(kotlin)
Next, the code creates Paint and Color objects to be used for drawing, sets a text size, and draws the page title text, including the current page number:
The text size is then reduced, and some body text is drawn beneath the title:
paint.textSize = 14f
canvas.drawText("This is some test content to verify that custom document printing works", leftMargin.toFloat(), (titleBaseLine + 35).toFloat(), paint)Code language:Kotlin(kotlin)
The last task performed by this method involves drawing a circle (red on even-numbered pages and green on odd). Having ascertained whether the page is odd or even, the method obtains the height and width of the page before using this information to position the circle in the center of the page:
Having drawn on the canvas, the method returns control to the onWrite() method. With the completion of the drawPage() method, the MyPrintDocumentAdapter class is now finished.
Starting the Print Job
When the user touches the “Print Document” button, the printDocument() onClick event handler method will be called. All that now remains before testing can commence, therefore, is to add this method to the MainActivity.kt file, taking particular care to ensure that it is placed outside of the MyPrintDocumentAdapter class:
package com.ebookfrenzy.customprint
.
.
import android.print.PrintManager
import android.view.View
classMainActivity : AppCompatActivity() {
funprintDocument(view: View) {
val printManager = this
.getSystemService(Context.PRINT_SERVICE) as PrintManager
val jobName = this.getString(R.string.app_name) + " Document"
printManager.print(jobName, MyPrintDocumentAdapter(this), null)
}
.
.
}Code language:Kotlin(kotlin)
This method obtains a reference to the Print Manager service running on the device before creating a new String object to serve as the job name for the print task. Finally, the print() method of the Print Manager is called to start the print job, passing through the job name and an instance of our custom print document adapter class.
Testing the Application
Compile and run the application on an Android device or emulator. When the application has loaded, touch the “Print Document” button to initiate the print job and select a suitable target for the output (the Save to PDF option is useful for avoiding wasting paper and printer ink).
Check the printed output, which should consist of 4 pages, including text and graphics. Figure 82-3, for example, shows the four pages of the document viewed as a PDF file ready to be saved on the device.
Experiment with other print configuration options, such as changing the paper size, orientation, and page settings within the print panel. The printed output should reflect each setting change, indicating that the custom print document adapter functions correctly.
Figure 82-3
Summary
Although more complex to implement than the Android Printing framework HTML and image printing options, custom document printing provides considerable flexibility in printing complex content within an Android application. Most of the work in implementing custom document printing involves the creation of a custom Print Adapter class that not only draws the content on the document pages but also responds correctly as the user changes print settings, such as the page size and range of pages to be printed.
As outlined in the previous chapter, entitled “Printing with the Android Printing Framework”, the Android Printing framework can print both web pages and dynamically created HTML content. While there is much similarity between these two approaches to printing, there are also some subtle differences that need to be considered. This chapter will work through the creation of two example applications to bring some clarity to these two printing options.
Creating the HTML Printing Example Application
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.
Enter HTMLPrint into the Name field and specify com.ebookfrenzy.htmlprint as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.
Printing Dynamic HTML Content
The first stage of this tutorial is to add code to the project to create some HTML content and send it to the Printing framework as a print job.
Begin by locating the MainActivity.kt file (located in the Project tool window under app -> java -> com .ebookfrenzy.htmlprint) and loading it into the editing panel. Once loaded, modify the code so that it reads as outlined in the following listing:
The code changes begin by declaring a variable named myWebView in which will be stored a reference to the WebView instance used for the printing operation. Within the printWebView() method, an instance of the WebView class is created to which a WebViewClient instance is assigned.
The WebViewClient assigned to the web view object is configured to indicate that loading the HTML content is to be handled by the WebView instance (by returning false from the shouldOverrideUrlLoading() method). More importantly, an onPageFinished() handler method is declared and implemented to call the createWebPrintJob() method. The onPageFinished() method will be called automatically when all HTML content has been loaded into the web view. As outlined in the previous chapter, this step is necessary when printing dynamically created HTML content to ensure that the print job is only started once the content has fully loaded into the WebView.
Next, a String object is created containing some HTML to serve as the content and subsequently loaded into the web view. Once the HTML is loaded, the onPageFinished() callback method will trigger. Finally, the method stores a reference to the web view object in the previously declared myWebView variable. Without this vital step, there is a significant risk that the Java runtime system will assume that the application no longer needs the web view object and will discard it to free up memory resulting in the print job terminating before completion. All that remains in this example is to implement the createWebPrintJob() method, which is currently configured to be called by the onPageFinished() callback method. Remaining within the MainActivity.kt file, therefore, implement this method so that it reads as follows:
privatefuncreateWebPrintJob(webView: WebView) {
val printManager = this
.getSystemService(Context.PRINT_SERVICE) as PrintManager
val printAdapter = webView.createPrintDocumentAdapter("MyDocument")
val jobName = getString(R.string.app_name) + " Print Test"
printManager.print(jobName, printAdapter,
PrintAttributes.Builder().build())
}Code language:Kotlin(kotlin)
This method obtains a reference to the PrintManager service and instructs the web view instance to create a print adapter. A new string is created to store the name of the print job (in this case, based on the name of the application and the word “Print Test”).
Finally, the print job is started by calling the print() method of the print manager, passing through the job name, print adapter, and a set of default print attributes.
Compile and run the application on a device or emulator running Android 5.0 or later. Once launched, the standard Android printing page should appear as illustrated in Figure 81-1.
Figure 81-1
Print to a physical printer if you have one configured, save to Google Drive, or select the option to save to a PDF file. Once the print job has been initiated, check the generated output on your chosen destination. Note that the system will request a name and location for the PDF file when using the Save to PDF option. The Downloads folder makes a good option, the contents of which can be viewed by selecting the Downloads icon (renamed Files on Android 8) located amongst the other app icons on the device
Creating the Web Page Printing Example
The second example application created in this chapter will provide the user an Overflow menu option to print the web page currently displayed within a WebView instance.
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Basic Views Activity template before clicking on the Next button.
Enter WebPrint into the Name field and specify com.ebookfrenzy.webprint as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.
Removing the Floating Action Button
Selecting the Basic Views Activity template provided a context menu and a floating action button. Since the app does not require the floating action button, it can be removed before proceeding. Load the activity_main.xml layout file into the Layout Editor, select the floating action button, and tap the keyboard Delete key to remove the object from the layout. Edit the MainActivity.kt file and remove the floating action button code from the onCreate method as follows:
override fun onCreate(savedInstanceState: Bundle?) {
.
.
// binding.fab.setOnClickListener { view ->// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)// .setAction("Action", null).show()//}
}Code language:JavaScript(javascript)
Removing Navigation Features
As A Guide to the Android Studio Layout Editor Tool outlines, the Basic Views Activity template contains multiple fragments and buttons to navigate from one fragment to the other. These features are unnecessary for this tutorial and will cause problems later if not removed. Before moving ahead with the tutorial, modify the project as follows:
Within the Project tool window, navigate to and double-click on the app -> res -> navigation -> nav_graph.xml file to load it into the navigation editor.
Within the editor, select the SecondFragment entry in the graph panel and tap the keyboard delete key to remove it from the graph.
Locate and delete the SecondFragment.kt (app -> java -> <package name> -> SecondFragment) and fragment_second.xml (app -> res -> layout -> fragment_second.xml) files.
Locate the FirstFragment.kt file, double-click on it to load it into the editor, and remove the code from the onViewCreated() method so that it reads as follows:
Edit the MainActivity.kt file and remove the following navigation code:
.
.
// private lateinit var appBarConfiguration: AppBarConfiguration
.
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
.
.
// val navController = findNavController(R.id.nav_host_fragment_content_main)// appBarConfiguration = AppBarConfiguration(navController.graph)// setupActionBarWithNavController(navController, appBarConfiguration)
}
.
.
// override fun onSupportNavigateUp(): Boolean {// // val navController = findNavController(R.id.nav_host_fragment_content_main)// return navController.navigateUp(appBarConfiguration)// || super.onSupportNavigateUp()//}Code language:JavaScript(javascript)
Designing the User Interface Layout
Load the content_main.xml layout resource file into the Layout Editor tool if it has not already been loaded and, in Design mode, select and delete the nav_host_fragment_content_main object. From the Widgets section of the palette, drag and drop a WebView object onto the center of the device screen layout. Click the Infer constraints toolbar button and, using the Attributes tool window, change the layout_width and layout_height properties of the WebView to match constraint so that it fills the entire layout canvas, as outlined in Figure 81-2 below.
Select the newly added WebView instance and change the ID of the view to myWebView.
Before proceeding to the next step of this tutorial, an additional permission must be added to the project to enable the WebView object to access the Internet and download a web page for printing. Add this permission by locating the AndroidManifest.xml file in the Project tool window and double-clicking on it to load it into the editing panel. Once loaded, edit the XML content to add the appropriate permission line, as shown in the following listing:
As with the project in the chapter entitled An Android Studio RecyclerView Tutorial we need to be able to use view binding to access a component (in this case myWebView) contained in the content_main.xml file from within the MainActivity class. To access views within the content_main.xml file, we again need to assign it an id at the point it is included. Edit the activity_main.xml file and modify the include element so that it reads as follows:
Before the web page can be printed, it must loaded into the WebView instance. For this tutorial, this will be performed by a call to the loadUrl() method of the WebView instance, which will be placed in a method named configureWebView() and called from within the onStart() method of the MainActivity class. Edit the MainActivity.kt file, therefore, and modify it as follows:
The option to print the web page will now be added to the Overflow menu. The first requirement is a string resource to label the menu option. Within the Project tool window, locate the app -> res -> values -> strings.xml file, double-click on it to load it into the editor, and modify it to add a new string resource:
Next, load the app -> res -> menu -> menu_main.xml file into the menu editor, switch to Code mode, and replace the Settings menu option with the print option:
With the onOptionsItemSelected() method implemented, the activity will call a method named createWebPrintJob() when the print menu option is selected from the overflow menu. The implementation of this method is identical to that used in the previous HTMLPrint project and may now be added to the MainActivity.kt file such that it reads as follows:
.
.
import android.print.PrintAttributes
import android.print.PrintManager
.
.
classMainActivity : AppCompatActivity() {
.
.
privatefuncreateWebPrintJob(webView: WebView?) {
val printManager = this
.getSystemService(Context.PRINT_SERVICE) as PrintManager
val printAdapter = webView?.createPrintDocumentAdapter("MyDocument")
val jobName = getString(R.string.app_name) + " Print Test"
printAdapter?.let {
printManager.print(
jobName, it,
PrintAttributes.Builder().build()
)
}
}
.
.
}Code language:Kotlin(kotlin)
With the code changes complete, run the application on a physical Android device or emulator. Once successfully launched, the WebView should be visible with the designated web page loaded. Once the page has loaded, select the Print option from the Overflow menu and use the resulting print panel to print the web page to a suitable destination.
Summary
The Android Printing framework includes extensions to the WebView class that allow printing HTML-based content from within an Android application. This content can be HTML created dynamically within the application at runtime or a pre-existing web page loaded into a WebView instance. In the case of dynamically created HTML, it is important to use a WebViewClient instance to ensure that printing does not start until the HTML has been fully loaded into the WebView.
Android Printing Framework is used to print content from within Android applications. While subsequent chapters will explore in more detail the options for adding printing support to your applications, this chapter will focus on the various printing options now available in Android and the steps involved in enabling those options.
The chapter will then provide an overview of the various printing features available to Android developers to build printing support into applications.
The Android Printing Architecture
The Printing framework provides printing in Android. In basic terms, this framework consists of a Print Manager and a number of print service plugins.
It is the responsibility of the Print Manager to handle the print requests from applications on the device and to interact with the print service plugins installed on the device, thereby ensuring that print requests are fulfilled.
By default, many Android devices have print service plugins installed to enable printing using the Google Cloud Print and Google Drive services. Print Services Plugins for other printer types, if not already installed, may also be obtained from the Google Play store.
Print Service Plugins are currently available for HP, Epson, Samsung, and Canon printers, and plugins from other printer manufacturers will most likely be released in the future. However, the Google Cloud Print service plugin can print from Android to just about any printer type and model. This book will use the HP Print Services Plugin as a reference example.
The Print Service Plugins
The purpose of the Print Service plugins is to enable applications to print to compatible printers that are visible to the Android device via a local area wireless network or Bluetooth.
The presence of the Print Service Plugin on an Android device can be verified by loading the Google Play app and performing a search for “Print Service Plugin”.
Once the plugin is listed in the Play Store, and if it is not already installed, it can be installed by selecting the Install button. Figure 80-1, for example, shows the HP Print Service plugin within Google Play.
The Print Services plugins will automatically detect compatible printers on the network to which the Android device is currently connected and list them as options when printing from an application.
Figure 80-1
Google Cloud Print
Google Cloud Print is a service provided by Google that enables you to print content onto your printer over the web from anywhere with internet connectivity. Google Cloud Print supports many devices and printer models in both Cloud Ready and Classic printers. A Cloud Ready printer has technology built-in that enables printing via the web. Manufacturers that provide cloud-ready printers include Brother, Canon, Dell, Epson, HP, Kodak, and Samsung. To identify if your printer is both cloud-ready and supported by Google Cloud Print, review the list of printers at the following URL:
In the case of classic, non-Cloud Ready printers, Google Cloud Print provides support for cloud printing by installing software on the computer system to which the classic printer is connected (either directly or over a home or office network).
To set up Google Cloud Print, visit the following web page and log in using the same Google account ID that you use when logging in to your Android devices:
Once printers have been added to your Google Cloud Print account, they will be listed as printer destination options when you print from within Android applications.
Printing to Google Drive
In addition to supporting physical printers, it is also possible to save printed output to your Google Drive account. When printing from a device, select the Save to Google Drive option in the printing panel. The content to be printed will then be converted to a PDF file and saved to the Google Drive cloud-based storage associated with the currently active Google Account ID on the device.
Save as PDF
The final printing option provided by Android allows the printed content to be saved locally as a PDF file on the Android device. Once selected, this option will request a name for the PDF file and a location on the device to which the document will be saved.
Both the Save as PDF and Google Drive options can be invaluable in terms of saving paper when testing the printing functionality of your own Android applications.
Printing from Android Devices
Google recommends that applications that can print content do so by placing the print option in the Overflow menu. Many applications bundled with Android now include “Print…” menu options. Figure 80-2, for example, shows the Print option accessed by selecting the “Share…” option in the Overflow menu of the Chrome browser application:
Figure 80-2
Once the print option has been selected from within an application, the standard Android print screen will appear, showing a preview of the content to be printed, as illustrated in Figure 80-3:
Figure 80-3
Tapping the panel along the top of the screen will display the full range of printing options:
Figure 80-4
The Android print panel provides standard printing options, such as paper size, color, orientation, and number of copies. Other print destination options may be accessed by tapping on the current printer or PDF output selection.
Options for Building Print Support into Android Apps
The Printing framework provides several options for incorporating print support into Android applications. These options can be categorized as follows:
Image Printing
As the name suggests, this option allows image printing to be incorporated into Android applications. When adding this feature to an application, the first step is to create a new instance of the PrintHelper class:
val imagePrinter = PrintHelper(context)Code language:Kotlin(kotlin)
Next, the scale mode for the printed image may be specified via a call to the setScaleMode() method of the PrintHelper instance. Options are as follows:
SCALE_MODE_FIT – The image will be scaled to fit within the paper size without cropping or changes to the aspect ratio. This will typically result in white space appearing in one dimension.
SCALE_MODE_FILL – The image will be scaled to fill the paper size with cropping performed where necessary to avoid the appearance of white space in the printed output.
Without a scale mode setting, the system will default to SCALE_MODE_FILL. The following code, for example, sets scale to fit mode on the previously declared PrintHelper instance:
Similarly, the color mode may also be configured to indicate whether the print output is to be in color or black and white. This is achieved by passing one of the following options through to the setColorMode() method of the PrintHelper instance:
COLOR_MODE_COLOR – Indicates that the image is to be printed in color.
COLOR_MODE_MONOCHROME – Indicates that the image will be printed in black and white.
The printing framework will default to color printing unless the monochrome option is specified as follows:
All that is required to complete the printing operation is an image to be printed and a call to the printBitmap() method of the PrintHelper instance, passing through a string representing the name to be assigned to the print job and a reference to the image (in the form of either a Bitmap object or a Uri reference to the image):
val bitmap = BitmapFactory.decodeResource(resources,
R.drawable.oceanscene)
imagePrinter.printBitmap("My Test Print Job", bitmap)Code language:Kotlin(kotlin)
Once the print job has been started, the Printing framework will display the print dialog and handle both the subsequent interaction with the user and the printing of the image on the user-selected print destination.
Creating and Printing HTML Content
The Android Printing framework also provides an easy way to print HTML-based content within an application. This content can either be HTML content referenced by the URL of a page hosted on a website or HTML content dynamically created within the application.
To enable HTML printing, the WebView class has been extended to include support for printing with minimal coding requirements.
When dynamically creating HTML content (as opposed to loading and printing an existing web page), the process involves the creation of a WebView object and associating with it a WebViewClient instance. The web view client is then configured to start a print job when the HTML has finished being loaded into the WebView.
With the web view client configured, the HTML is loaded into the WebView, and the print process is triggered. Consider, for example, the following code:
The code in this method begins by declaring a variable named myWebView in which will be stored a reference to the WebView instance created in the method. Within the printContent() method, an instance of the WebView class is created to which a WebViewClient instance is assigned.
The WebViewClient assigned to the web view object is configured to indicate that the loading of the HTML content is to be handled by the WebView instance (by returning false from the shouldOverrideUrlLoading()) method. More importantly, an onPageFinished() handler method is declared and implemented to call the createWebPrintJob() method. The onPageFinished() callback method will be called automatically when all HTML content is loaded into the web view. This ensures that the print job is not started until the content is ready, ensuring that all content is printed.
Next, a string is created containing some HTML to serve as the content. This is then loaded into the web view. Once the HTML is loaded, the onPageFinished() method will trigger. Finally, the method stores a reference to the web view object. Without this vital step, there is a significant risk that the Java runtime system will assume that the application no longer needs the web view object and will discard it to free up memory (a concept referred to in Java terminology as garbage collection), resulting in the print job terminating before completion. All that remains in this example is to implement the createWebPrintJob() method as follows:
privatefuncreateWebPrintJob(webView: WebView) {
val printManager = this
.getSystemService(Context.PRINT_SERVICE) as PrintManager
val printAdapter = webView.createPrintDocumentAdapter("MyDocument")
val jobName = getString(R.string.app_name) + " Document"
printManager.print(jobName, printAdapter,
PrintAttributes.Builder().build())
}Code language:Kotlin(kotlin)
This method obtains a reference to the PrintManager service and instructs the web view instance to create a print adapter. A new string is created to store the name of the print job (which, in this case, is based on the name of the application and the word “Document”).
Finally, the print job is started by calling the print() method of the print manager, passing through the job name, print adapter, and a set of default print attributes. If required, the print attributes could be customized to specify resolution (dots per inch), margin, and color options.
Printing a Web Page
The steps involved in printing a web page are similar to those outlined above, with the exception that the web view is passed the URL of the web page to be printed in place of the dynamically created HTML, for example:
It is also important to note that the WebViewClient configuration is only necessary if a web page is to automatically print as soon as it has loaded. If the printing is to be initiated by the user selecting a menu option after the page has loaded, only the code in the createWebPrintJob() method outlined above needs to be included in the application code. The next chapter, An Android Studio HTML and Web Printing Example, will demonstrate such a scenario.
Printing a Custom Document
While the HTML and web printing features introduced by the Printing framework provide an easy path to printing content from within an Android application, it is clear that these options will be overly simplistic for more advanced printing requirements. The Printing framework also provides custom document printing support for more complex printing tasks. This allows content in the form of text and graphics to be drawn onto a canvas and then printed.
Unlike HTML and image printing, which can be implemented with relative ease, custom document printing is a more complex, multi-stage process which will be outlined in the An Android Studio Custom Printing Tutorialchapter of this book. These steps can be summarized as follows:
Connect to the Android Print Manager
Create a Custom Print Adapter sub-classed from the PrintDocumentAdapter class
Create a PdfDocument instance to represent the document pages
Obtain a reference to the pages of the PdfDocument instance, each of which has associated with it a Canvas instance
Draw the content on the page canvases
Notify the print framework that the document is ready to print
The custom print adapter outlined in the above steps must implement several methods that the Android system will call upon to perform specific tasks during printing. The most important of these are the onLayout() method, responsible for re-arranging the document layout in response to the user changing settings such as paper size or page orientation, and the onWrite() method which is responsible for rendering the pages to be printed. The chapter entitled An Android Studio Custom Printing Tutorialwill cover this topic in detail.
Summary
The Android SDK can print content from within a running app. Print output can be directed to suitably configured printers, a local PDF file, or to the cloud via Google Drive. From the perspective of the Android application developer, these capabilities are available for use in applications by making use of the Printing framework. By far, the easiest printing options to implement are those involving content in the form of images and HTML. More advanced printing may, however, be implemented using the custom document printing features of the framework.