Chapter 4

Defining an App’s Behavior

IN THIS CHAPTER

check Interacting with users through notifications

check Ensuring that data remains safe and user accessible

check Working with various kinds of data

check Accessing special device features

The best app in the world would be unnoticeable except for the service it provides. When you can focus completely on the task at hand and not even notice the app that is helping you perform that task, you have a really great app. The problem is that most apps don’t come close to this ideal because they’re actually quite intrusive. They shout, “Here I am. Aren’t I truly amazing?” Even a well-behaved app does need to provide notifications at times, but those notifications should come only when they’re welcome and expected. This chapter shows you effective ways to work with notifications, permissions, and preferences — three of the cornerstones of great app behavior.

Apps can perform a lot of work for you or help you perform the work yourself, but most of that work somehow involves data. Many users don’t actually consider how much data they interact with daily, but the amount of data that people plow through is truly amazing. An app that can manage data seamlessly, in a manner that lets the user see a task rather than data, is providing an essential service. This chapter doesn’t tell you everything there is to know about data, but you get an overview of some data management techniques specifically for Android.

Along with useful behaviors and data management, users also expect apps to provide access to the latest gadgetry installed on the host device. When you look through the specifications for modern smartphones, imagining how you might even use some of the gadgets is hard. It’s sort of like having one of those knives with 20 different blades and attachments, including a wine cork puller. Having apps that not only access these gadgets without problem, but also suggest uses for them, is really something. This chapter delves into the common camera and the functionality that allows two users to share data, but you get enough of a feel for gadget access to add some gadget support to your new app as well.

Working with Notifications

Notifications are messages that appear outside the app’s UI to tell you something potentially useful. However, many notifications are annoying rather than helpful. For example, you’re in the middle of a meeting and your phone buzzes to tell you the weather has changed. It’s absolutely lovely out there; don’t you wish you were outside? After gritting your teeth, you turn off that message forever only to have another immediately pop up. Your grocery is running a special on plastic bags — you’d better get to the store now and buy scads of them! Once again, you turn off that notification … and another arrives. The notifications are annoying, and now you’ve missed something important that your boss said. (Something about people being let go? Was that it?)

The following sections talk about notifications of all sorts, hopefully useful ones rather than the other kind. You also learn about Do Not Disturb mode, which should actually mean no disturbances.

Understanding what notifications do

Notifications are supposed to communicate. They might tell you of an emergency or about the number of messages that an app is currently holding for you. A notification can tell you that a loved one urgently needs to talk with you or that you’re going to be late paying a bill if you don’t get going. So the type of communication, why you’re receiving the communication, and the content of the communication can vary widely. However, you generally see notifications presented in one or these ways:

  • An icon in the status bar
  • A more detailed entry in the notification drawer
  • A badge on the app's icon
  • Automatically as a pop-up
  • On the device’s sign-in screen
  • As a dialog box when you start the app

How a notification presents itself can often tell you something about the notification when the app uses notifications correctly. For example, you can see an icon on the status bar at all times, making it a good option for high-priority communication. The only higher-priority notification type might be the pop-up, which should be used rarely (or not at all) because it’s incredibly annoying and intrusive. You’d use a pop-up only for the most essential information, like telling someone that her hair is on fire.

Remember A badge on the app’s icon is the most appropriate form of notification for most purposes. The user will generally have to focus attention on the app to see it, so the notification tells the user that there’s something important to know about without being intrusive. However, if your app provides a general service for the device as a whole, such as essential gadget support, placing the notification in the notification drawer is also an option.

Perhaps the most annoying notifications of all (except those that are telling you about a bona fide emergency, such as a meteor is about to fall on your head) are the ones that buzz the phone or flash the device’s LED. In addition to running rampant on your screen, they’re now distressing you in other ways that are almost certainly going to be noticed by other people, who will politely laugh behind your back. To avoid these and other well-intentioned but annoying, notifications check out the guide at https://material.io/design/platform-guidance/android-notifications.html#usage. The guidelines tell you about things you must avoid, such as cross-promotion of another product within the notification, or sending notifications from apps that the user has never opened, because they’re strictly prohibited by the Google Play Store.

Remember Some notifications are required. For example, in Chapter 3 of this minibook, you see how to work with WorkManager. Because the background processes it creates can use both battery and data, you must provide a notification that the background process is running and its percentage of completion. The notification must also provide the means of stopping the background process should the need arise. This requirement also affects things like DownloadManager. The way you manage alarms with AlarmManager is somewhat different because there is an alarm, not a process, but you still need to provide some sort of notification — an icon in the status bar or a badge on the app so that the user knows the alarm is active and can stop it if necessary. You don't require a notification for background tasks, such as updating a progress bar (see the “Creating a test app” section of Chapter 2 of this minibook), because they remain active only while the app runs.

Anatomy of a notification

A notification is a kind of highly formatted dialog box when displayed. There are differences, but if you start by viewing them as special dialog boxes as a developer, you can better understand some of the concepts behind them. The formatting consists of these items:

  • Heading: The heading is a small strip at the top of the notification that acts as an identifier of notification intent and purpose. It contains these elements:
    • App icon: People readily identify apps that they commonly use by the app icon. For example, most people can identify Twitter and Facebook solely by the icons their apps use.
    • App name: Some organizations create more than one app but use similar icons for each app. The app name identifies a particular app.
    • Header text (optional): Header text is a short piece of information to identify the sender when multiple people, organizations, devices, or other sources use the same app for notifications. For example, the header might contain the email address of the sender.
    • Time stamp: Even though this element doesn’t appear by default and Google claims that it’s optional, it really isn’t optional. The most annoying kind of notification is one that’s two weeks old and no longer important. Keep your users happy; include a time stamp.

      Tip Fortunately, you don’t have to rely on a time stamp alone. You have the ability to programmatically dismiss an outdated notification before the user sees it. This is especially important when dealing with notifications in the notification drawer.

    • Expand indicator: This is a little down-pointing arrow that tells the recipient that there is more information. Clicking the arrow expands the content to display the additional content (at which point the down-pointing arrow becomes an up-pointing arrow so that the user can close the notification).
  • Primary content: The primary content area contains the message you want to convey. It normally contains these elements:
    • Content header: The content header should appear in slightly larger and usually bold text than the rest of the notification. It should provide an extremely brief bit of text that you use to convince the viewer to read more.

      The header can also indicate multiple notifications from a single app. For example, when working with an email app, the content header can indicate the number of pending emails in the user’s Inbox.

    • Content text: This is the actual message. You should be brief enough to give the viewer an idea of why the notification is important without having to expand it. Of course, you can always provide an expand indicator if you need additional for detailed content.
  • Origin indicator: This is a relatively large icon that tells you about the message origin. Although you often see someone’s picture here, some notifications provide organization logos or other indicators of the notification source. The idea is that if you see the face of a friend staring at you from a notification, you’re more likely to read it.
  • Actions: You always need an action to get rid of the notification. Nothing is more annoying than to have a notification that hangs around like gum on your shoes. Other actions you might include are the ability to reply to the notification; open the app that generated the notification; call the person who sent the notification; or go to a website to obtain additional information. The point is not to add a confusing array of actions that have nothing to do with the notification.

    One important action is the hierarchy indicator. When an app sends out multiple notifications, but these notifications represent a hierarchy, such as a message thread, the hierarchy indicator tells how many child notifications there are for the current notification so that the user can decide whether to drill down into the hierarchy to learn more.

    You can also make one of the actions be a short response from the user. The user can type text into the notification to provide a short answer to a question, such as yes or no to an invitation to dinner.

Remember You need to be a good citizen when it comes to notifications. The user should have the ability to tell your app that it doesn’t need to send additional notifications. Precisely how the user configures an app to stop notifications depends on the version of Android used, but it’s essential to heed any user settings.

Assigning a channel to your notification

When viewing a television, you select a channel to determine what will appear on the screen. The channel is important for this reason. Likewise, when you create Android notifications, you also assign a channel to each unique notification. For example, if your email app includes notifications for new mail and for upcoming appointments, you need two channels: one for each notification. The use of separate channels means that users can block just one notification type and keep the rest.

When deciding on how to create channels, consider these issues:

  • When developing notifications, consider whether some notifications belong to a single, well-defined group, such as download completion. After all, it doesn’t matter what you’re downloading; the important issue is that the download has completed.
  • Also a good idea is to use the same importance level for all notifications you have grouped together. If a particular download requires a higher importance, you should provide a separate channel for it.
  • Always create channel groupings when the number of channels your app supports exceeds ten. You don’t want to bury your user in unique notifications that end up not being all that unique.
  • Use the same channels for all users when your app supports multiple users.
  • Link your channels to specific app settings so that the user can control channels individually.

Android actually supports two scopes of channel. You always configure an app-specific channel for your notification. In addition to the app-specific channel, you can create a system-wide notification channel by assigning your notification to one of the predefined categories using NotificationCompat.Builder.setCategory():

  • CATEGORY_CALL: An incoming call or other life communication request (as potentially differentiated from an email)
  • CATEGORY_MESSAGE: Any incoming direct message such as SMS or an instant message
  • CATEGORY_EMAIL: Any incoming asynchronous message, usually in bulk form, such as an email
  • CATEGORY_EVENT: Any sort of an event, such as one found on a calendar
  • CATEGORY_PROMO: A promotion or advertisement
  • CATEGORY_ALARM: An indicator from an alarm or timer (including one from an outside source, such as your home alarm)
  • CATEGORY_PROGRESS: An indicator that a long-running background process has achieved a particular milestone or goal
  • CATEGORY_SOCIAL: Any type of social network, resource sharing, or information update communication
  • CATEGORY_ERROR: A notice that something bad has happened to an app, background operation, authentication, device, or something else equally regrettable in its effects
  • CATEGORY_TRANSPORT: Any type of notification (not necessarily bad) related to the media transport control for playback
  • Warning CATEGORY_SYSTEM: A notification reserved for system use that you shouldn't ever use unless you’re one of the few who work with the Android operating system or a piece of special software like a device driver

  • CATEGORY_SERVICE: A general category for background tasks that doesn’t relate to the process progress (CATEGORY_PROGRESS) or a process error (CATEGORY_ERROR)
  • CATEGORY_RECOMMENDATION: A recommendation of any sort, including information like news stories that a person might want to read or sources of additional information for working with an app (not to be confused with a promotion, which is covered by CATEGORY_PROMO)
  • CATEGORY_STATUS: An update about the status of an app, device, or other items of interest (such as a notice telling you that device support for a new SD card is installed)

Setting the notification importance

Some notifications are more important than others, and notifications come with these importance-level restrictions:

  • Default: When creating your notification, you can assign an importance level to each channel based on how important you feel the notification is to the user. This is the default importance level.
  • User-assigned: The problem with assigning a default importance level comes in when you feel that a notification is at one level and the user has a different opinion. Obviously, the user should win, which means that the user should be allowed to change the importance level in Settings.
  • Programmatically assigned: The default level remains in effect until the user changes it. After the user has changed the default level through Settings, you may lower the importance level but you may not increase it through your app. This restriction keeps the app from deciding that the user really does need to see the notification as important after all.

Android reacts differently to your notification based on the notification level you assign to it, so choosing the correct notification level is important. Choosing the wrong notification level can prove extremely annoying to the user, not to mention interrupt something that's actually more important. Here are the importance levels:

Level

What Happens

Uses

HIGH

Makes a sound and appears onscreen

Information that the user must act upon immediately, such as text messages, alarms, and phone calls. And yes, this is where you put those emergency notifications, such as alerting users to that tornado heading their way.

DEFAULT

Makes a sound and shows an icon in the status bar

Information the user should review as soon as convenient, such as traffic alerts and task reminders.

LOW

Nothing except an icon in the status bar

Content the user may or may not want to review from apps the user has subscribed to as well as noncritical invitations from friends (such as an impromptu party).

MIN

A badge on the app, or the notification simply appears when the user opens the app

Content that the user is less likely to act upon immediately, even when opening the app, such as pointing out interesting app features, describing points of interest in a town, weather updates, and promotional content.

Considering the notification types

There are two kinds of notifications. Both of them should be optional and allow the user to opt in, opt out, or pause the notifications as needed. However, most developers view the transactional notifications as somewhat required depending on what sort of information they convey. Here's an overview of notification types:

  • Transactional: Transactional notifications require instant action on the part of the recipient. You can group them according to these needs:
    • Immediate interaction: Normally this is viewed as human-to-human contact, such as a telephone call or the need to perform a particular task with another person, but you could just as easily be interacting with a computer. For that matter, the interaction might be completely automated, such as a message sent to a first responder. The point is that the need is immediate.
    • Functional requirements: These notifications might tell you about a pending appointment, the status of your flight, or other issues that help you function better as a person. The notification could also come from your home’s alarm system or tell you that a camera near your home has detected movement. The point again is that the need to view the data is immediate: You can’t put off detecting a burglar or reacting to a fire.
    • Device interaction, management, and control: These sorts of notifications help you deal with apps like your music player, or they help you decide what to do when you’ve taken a picture with your camera. You also rely on these notifications to tell you about the state of apps running in the background.
  • Nontransactional: Nontransactional notifications are informative, but you don’t necessarily have to see them right away. Your app should make these notifications optional in all cases, and there are other rules you must follow, such as not providing a notification at all until the user has opened the app the first time. You can group such notifications according to these needs:
    • App maintenance and upgrade: You can inform the user about new versions of your app or updates that are available for your app. Also, you can provide noncritical notifications of app behavior issues and so on.
    • Usage tips: Some apps offer usage tips so that users can have a better experience when working with them.
    • Surveys and other input: App developers work hard to give you a great app experience and may ask you to rate their app or fill out a survey.
    • Hardware updates: After installing a new piece of hardware, you may see a notification that tells you about how the hardware will make your device better or that the operating system has installed support for it.

Relying on notification updates

Your user doesn’t want to be bombarded by notifications, especially when one notification is simply an update of another notification that you sent earlier. Fortunately, you can update a notification so that a single notification serves the purpose. Update strategies for some notification types are obvious: A progress notification can simply update the progress bar it contains. However, when working with other notification types, you can adopt one of these strategies for updates:

  • Reissue the existing notification with modified content.
  • Use the Inbox style of notification for things like conversations or traffic updates (see https://developer.android.com/reference/android/app/Notification.InboxStyle for a discussion of Notification.InboxStyle).
  • Rely on a notification group with an associated summary. (The system uses this option even if you don't specify a group after the app sends four or more of the same kind of notification.)

Do Not Disturb mode

Users rely on Do Not Disturb mode to keep them from looking at their device at inconvenient times, such as when the boss is talking about a promotion or they’re proposing marriage. Of course, if your app generates notifications during this time and it isn’t important, the user is likely to be incredibly unhappy. A user has a number of options for setting a Do Not Disturb mode configuration, but the article at https://www.digitaltrends.com/mobile/do-not-disturb-mode-in-android/ gives you a good idea of what they are.

Remember As a developer, you can choose to ignore the Do Not Disturb mode settings with certain restrictions. When assigning your notification to the CATEGORY_ALARM, CATEGORY_REMINDER, CATEGORY_EVENT, or CATEGORY_CALL system-wide channel categories, Android determines whether to disturb a user, even when in Do Not Disturb mode, based on the user's settings. The user still has final say over whether your app can disrupt Do Not Disturb mode, so you shouldn’t count on a notification being received at any particular time.

Creating a notification

You create a notification to tell the user about something special. Android uses a four-step process to accomplish this task:

  1. Create a notification channel using the guidelines in the “Assigning a channel to your notification” section of the chapter.
  2. Register the notification channel with Android.
  3. Create a notification using the guidelines outlined in the “Anatomy of a notification” section, earlier in this chapter.
  4. Send a notification using NotificationManager.

To get started in creating a notification, create a new app project named 03_04_01 using the same techniques as those in Book 1, Chapter 3. Use an Empty Activity as a starting point. However, because of the way notifications work in newer versions of Android, you must set a minimum API level of 26. This example requires just one button, named SendNotification.

Making an icon

One of the problems that seem to plague Android developers is getting their icon to display properly, if at all. You must provide an icon with your notification or the app won't run, so putting off the icon issue until later won’t work.

A common issue is that the icon is the wrong size. The website at https://www.creativefreedom.co.uk/icon-designers-blog/android-4-1-icon-size-guide-made-simple/ has a full list of icon sizes. The best way to go for testing purposes is to create a 24-x-24-pixel icon. You can always create other sizes to support other devices later, but the 24-x-24-pixel icon will work with everything.

Tip A more difficult issue to deal with than icon size is that the icon background must be transparent. Depending on your graphics application, you may need to set a special transparent configuration option when creating a new file. The image itself will usually show some sort of pattern to let you know that the background is transparent, as shown in Figure 4-1. If your graphics application supports both raster and vector graphics, you must configure the icon for a raster setting. You also need to know that any color you use in creating the icon won’t show up when you see it in Android, as you see later in the chapter.

Image of an icon of the letter 'P' depicting some sort of pattern to let you know that the background is transparent.

FIGURE 4-1: An icon with a transparent background.

Creating the notification channel

Placing the code for creating a notification channel in a separate function is a good idea because you’ll use this function repeatedly while your app runs if you issue notifications regularly. The following code shows a basic createNotificationChannel() function that offers a little flexibility, such as whether to show a badge, but presets other features, such as the notification source:

fun createNotificationChannel(
context: Context, importance: Int, showBadge: Boolean,
name: String, description: String) {

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Toast.makeText( this, "API Too Old",
Toast.LENGTH_LONG)
return
}

val channelId = "${context.packageName}-$name"
val channel = NotificationChannel(channelId, name,
importance)
channel.description = description
channel.setShowBadge(showBadge)

val notificationManager = context.getSystemService(
NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}

This createNotificationChannel() function is designed for using with Android API versions 26 and up — the Android-O notification (see https://medium.com/exploring-android/exploring-android-o-notification-channels-94cd274f604c for details). You should use this notification type unless you absolutely have to support older devices. The first part of the createNotificationChannel() function checks whether the caller has an Android-O capability. If not, the example displays a Toast and exits. At some point, you need a better strategy for a production app, but this strategy works for now.

The next steps begin to configure the notification channel. You need a channelId, which is usually the package name and the name of the channel (not the notification). This setting is the in-app channel, not the system-wide channel setting.

The code calls on the NotificationChannel() constructor to create the channel, which includes the NotificationManagerCompat importance level, such as NotificationManagerCompat.IMPORTANCE_DEFAULT. To finish the channel setup, the code defines whether to show a badge and provides a description. The call to notificationManager.createNotificationChannel() completes the process of creating a channel based on the characteristics given. To use this function, you add the following code that appears in bold to the app's onCreate() function.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

createNotificationChannel(this,
NotificationManagerCompat.IMPORTANCE_DEFAULT,
false, getString(R.string.app_name),
"App Notification Channel")
}

Defining and sending a notification

Using the notification channel to actually send a notification comes next. The onSendNotificationClick() function handles a user click from the button, as shown here.

fun onSendNotificationClick(view: View) {
val name = getString(R.string.app_name)
val channelId = "${this.packageName}-$name"

val notificationBuilder = NotificationCompat.Builder(
this, channelId).apply {
setSmallIcon(R.drawable.pizza)
setContentTitle("Your Pizza is Ready")
setContentText("Get ready to eat a pizza!")
setStyle(NotificationCompat.BigTextStyle()
.bigText("Get ready to eat a pizza!"))
priority = NotificationCompat.PRIORITY_DEFAULT
setAutoCancel(true)

val intent = Intent(applicationContext,
MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(
applicationContext, 0, intent, 0)
setContentIntent(pendingIntent)
}

val notificationManager = this.getSystemService(
NotificationManager::class.java)
notificationManager.notify(5,
notificationBuilder.build())
}

Remember Even though some elements of this process look similar to those used to create a channel, what you're really building is a notification, so you still begin by performing tasks like getting the app name and the package name to create a channelId.

The notificationBuilder object doesn't actually send the notification; instead, it builds the notification so that you can send it. To start the building process, you call NotificationCompat.Builder() and supply it with the current context and the channelId. The call to apply begins defining the notification message. All the elements shown in the code are essential to this notification template. If you leave out the call to setSmallIcon(), for example, the code will fail with an exception. This is a default priority notification, so you use the NotificationCompat.PRIORITY_DEFAULT setting. The notification also automatically cancels after a time.

The Intent is essential as well. This is a basic notification, so this intent merely opens the app's MainActivity when the user clicks the notification in the notification drawer. Even if the app is already running, the MainActivity will receive focus. Clicking the notification also clears it.

All this code goes toward creating a notification that you haven't sent yet. To send the notification, the code begins by creating a NotificationManager object. It then calls notificationManager.notify() with the number of the notification and the actual notification object. The notification number is for the app's use in updating the notification later; Android doesn’t care what number you assign. Notice that you must call notificationBuilder.build() to actually create the notification. When this process is complete, you see a notification on the status bar like the one shown in Figure 4-2.

Remember The first thing you should notice about the P (for pizza) icon on the status bar is that it’s white. It has no color as it does in Figure 4-1. If the background of your icon is colored, even a different color from the foreground, what you see is a white square, not an icon. This is why you must make the background of your icon transparent.

When the user pulls out the notification drawer, the notification content can be seen, as shown in Figure 4-3. The content is rudimentary, but when you click the notification, you see the MainActivity as expected, and the notification goes away.

A mobile screen displaying a notification on the status bar when the user pulls out the notification drawer.

FIGURE 4-2: Seeing the notification on the status bar.

A mobile screen displaying a big text template of the notification as it appears in the notification drawer.

FIGURE 4-3: The notification as it appears in the notification drawer.

Figure 4-3 shows a basic example of just one template, the big text template, as defined by the call to NotificationCompat.BigTextStyle(). Android provides more than one template to use for notifications, and you should choose the most appropriate type. To use other styles, you simply choose a different NotificationCompat function (as shown at https://developer.android.com/reference/android/support/v4/app/NotificationCompat). Here's an overview of the various notification templates:

  • Standard: This is the standard notification that contains just a short amount of information and one or two actions.
  • Big text: You use this template when you have a lot of text to display. It looks similar to the standard template, except that it allows room for a lot more information.
  • Big picture: In addition to the icon, this template provides space for a picture of some type. You can use it for things like screen captures or to see a picture that someone sent you.
  • Progress: Contains a progress bar that you can update to show the progress of something like a download.
  • Media: Used to control media playing in the background. You see the standard VCR controls and possibly a picture of the album cover.
  • Messaging: This template includes a picture of the person sending the message. The message could be a text, but it has potential for other uses as well.
  • Custom: If none of the templates in this list gives you what you need, you can always try to create a custom template. The article at https://developer.android.com/training/notify-user/custom-notification describes how to create a custom template.

Getting Permission

The “Performing Background Tasks Using WorkManager” section of Chapter 3 of this minibook writes data to external storage. Android locks down the system, so you often find yourself obtaining permission for some need. For example, if you have a time-sensitive notification that you want to display in full-screen mode, you must have the USE_FULL_SCREEN_INTENT permission to do it. The following sections provide you with an overview of how to work with permissions. Many of the book examples require permissions so that you can see them in use.

Considering permission use

No matter what you want permission to do, you follow the same set of steps to obtain and verify the permission:

  1. Add a permission entry to AndroidManifest.xml. The problem is that you often need more than one permission. In the WorkManager example in Chapter 3, you need permission to both read and write external storage because part of the process verifies that the file doesn't currently exist, which requires reading.
  2. Use ActivityCompat.requestPermissions() to request the permission in your app code. When working with multiple permissions, you can make a single request using an array of permissions.
  3. Verify that the permission is granted using ContextCompat.checkSelfPermission(). You may think you have permission when you really don't. Error messages during the app run sometimes hide permissions issues in some other form, such as by saying a file system is read-only when the real problem is not having write permission.

Getting the permission is generally not hard unless you request something at the system level or something that is particularly dangerous. However, whether you should ask for the permission in the first place can be difficult to decide. Here are some best practices to consider:

  • Obtain only the permissions you absolutely must have. If possible, try to find other ways to perform the task that don’t require the permission.
  • Use libraries that require as few permissions as possible. When you use a library, you also inherit the library’s permissions requirements.
  • Consider precisely how you use the permission so that you can make a case for doing so with the user. Android will ask the user about granting certain permissions. A user who doesn’t have enough information can’t make an informed decision.
  • Make any access to anything outside the app apparent. For example, when you download or save a file, display a message saying that you’re performing these tasks.

Anything you can do to keep the user, the app, the device, and the data safe will make it easier for you to maintain a good reputation and obtain better reviews. More important, users can revoke permissions at any time, so it pays to be a good citizen. If the user revokes a needed permission, your app will suffer a loss of functionality, which is why you should always verify that a permission is available before using it and then act accordingly.

Tip It pays to test your app in an environment where the user hasn’t granted one or more permissions or revoked them after granting them. Otherwise, you can’t be sure that your app won’t crash due to a lack of permissions you thought you had.

Configuring permissions in AndroidManifest.xml

The act of gaining permission to do something starts with the AndriodManifest.xml file. You include a <uses-permission> element in the <manifest> element rather than the <application> child element. A typical entry appears as an attribute of the <uses-permission> element, like this:

android:name="android.permission.WRITE_EXTERNAL_STORAGE"

Including the entry in AndroidManifest.xml isn't enough, however. You must also include a call to ActivityCompat.requestPermissions() with an array of permissions, like this one:

android.Manifest.permission.WRITE_EXTERNAL_STORAGE

The form of both permission entries is similar. You obtain the names of the permissions you need from https://developer.android.com/reference/android/Manifest.permission. When you look at the details of a permission, you find notes about usage. For example, you don’t need the WRITE_EXTERNAL_STORAGE permission to write files in the application-specific directories returned by a call to Context.getExternalFilesDir() or Context.getExternalCacheDir() when working with API level 19 or above.

Remember One of the most important entries in the permission documentation is the entry entitled Protection Level. This entry tells you how Android views a particular permission and what is likely to happen with regard to user interaction when you request it. Android supports four protection levels, three of which you see used in third-party apps:

  • Normal: Covers areas where you need to access resources outside the app's sandbox, but the resource is unlikely to affect user privacy or the workings of other apps. For example, when you request access to time or time zone information, you have little chance of a security breach. The system grants this level of permission when it installs the app and the user isn’t alerted to the need for the resource access.
  • Signed: To use a signed permission, your app must have a certificate that is signed by the same app that defines the permission. In other words, the other app must trust your app to grant this permission, and there is a chance that your app could interfere in some way with the other app.

    Remember Some signed permissions aren’t meant for use by third-party apps. Of course, documentation being what it is at times, you might find that you don’t quite realize that you’re not supposed to use the resource in question. This is a good place to look for additional information when you can’t solve a permissions problem in other ways.

  • Dangerous: Any permission that could cause a loss of user permission, damage data stored on the local media, interfere in some minor way with the underlying operating system, or interfere with other apps is dangerous. To use a dangerous permission, you must obtain user approval. The user sees a dialog box that tells about the permission and asks whether the user wants to grant or deny the permission. The example in the “Performing Background Tasks Using WorkManager” section of Chapter 3 of this minibook uses a dangerous permission. You can also read more about dangerous permission prompts at https://developer.android.com/guide/topics/permissions/overview#dangerous-permission-prompt.
  • Special: Some permissions, such as SYSTEM_ALERT_WINDOW and WRITE_SETTINGS, are so sensitive that your app should avoid using them. These settings can affect the underlying operating system and cause other problems for the user. If your app needs one of these permissions, the system displays a detailed management screen to the user instead of the usual permissions dialog box. The user must grant permission. In some cases, the system will simply deny a special permission because the permission isn't available to third-party apps under any condition.

    Technical Stuff In addition to the special screen and enhanced system monitoring, a special permission generally requires special permission check and usage calls. For example, if you want to use the WRITE_SETTINGS permission, you must check whether the user has granted permission by calling Settings.System.canWrite(). Be sure you understand any special requirements for using a special permission before attempting to request one.

Complying with User Preferences

Your app has to interact with the user of the app to ensure that it works as anticipated. In some cases, such as notifications, the system performs this task for you, as shown in Figure 4-4. Using these system-supplied settings, the user can choose not to receive notifications from your app.

A mobile screen depicting that your app has to interact with the user of the app to ensure that it works as anticipated.

FIGURE 4-4: Notification reception preferences are automatic.

The user can also choose whether to grant permissions. Your app can request a permission and the user can grant it at the outset, but by using the Permission manager in Figure 4-5, the user can choose to revoke the permission later. However, in other situations, you must provide the settings for the user by relying on built-in Android functionality as described in the sections that follow.

Deciding on a preference set

Preferences indicate a user's choice in how to perform a task or configure an interface. Giving the user choices helps the user feel in control of the app and makes the app usage experience nicer. Attitude can make a huge difference in how the user views your app, so providing choices can also boost user acceptance. In some cases, you must offer preferences just to make your app work. For example, a user can’t buy anything unless you know where to send the item, which means creating a preference that allows access to one or more shipping addresses.

With all these benefits of providing preferences in mind, you might initially think that a huge preference set that allows the user to address every aspect of the app is what you need, but that’s not a useful strategy because it leads to user confusion. Here are some considerations for building a preference set for your app:

A mobile screen depicting that your app can request a
permission and the user can grant it at the outset, using the Permission manager.

FIGURE 4-5: A user can revoke permissions after the fact.

  • Provide options for communicating with your organization in various ways.
  • Avoid adding settings for configuration items that the user can set up in other ways, such as notifications and security.
  • Create a reasonable list of user interface options, such as color, text size, and so on, but don’t include settings that will reorganize the user interface in ways that will make it hard to support.
  • Allow the user to choose where, how, and when to store personal data.
  • Make it easy to remove historical data that isn’t necessary for app functioning.
  • Define a method for sharing preferences with others when appropriate.
  • Use online preference storage when appropriate for apps that might be used by a single user on multiple devices.
  • Keep personal data to a minimum and use best practices to keep personal data safe.

Tip When working with settings, the Settings option should appear as part of the navigation or on the app menu. Placing the Settings option anywhere else is likely to confuse the user.

Setting preferences using the Preference Library

You have a number of choices when creating a way to interact with the user for preferences. The method used until recently was to create a PreferenceFragment; however, according to the documentation at https://developer.android.com/reference/android/preference/PreferenceFragment, this method is now deprecated. The example in this section shows how to use the Preference Library approach instead. If you aren’t supporting API level 28 and above in your app, however, you can still use the PreferenceFragment approach as described at https://guides.codepath.com/android/settings-with-preferencefragment.

Designing a Preferences dialog box can also take two approaches:

  • XML: The layout for the Preferences dialog box appears in an XML file that you inflate much the same as you do with a fragment. Because this is the method you use for both activity and fragment layouts, it will probably feel like the most natural approach.
  • Coded: The layout appears as code within the app. If you go this route, make sure you create a separate function to hold the layout code to make it easier to change later. Some developers simply prefer code, and this is a perfectly acceptable method.

The example in this chapter takes the XML approach because it's easier to change and understand in most situations. The coded approach can become hard to decipher and you may not get the results you want.

Create a new app project named 03_04_02 using the same techniques you did in Book 1, Chapter 3. Use a Basic Activity as a starting point, instead of the Empty Activity template used in previous examples. The Basic Activity template includes a Settings option right on the menu, so you save yourself a lot of work. Change the default TextView component to have an Id of UserName and a text value of Your Name. The following sections show how to create this example.

Performing the required setup

As with many of the features you add to an Android app, you must provide a Gradle entry to create preferences. Open the build.gradle (Module: app) file and add the following entry to the dependencies section:

implementation 'androidx.preference:preference:1.1.0'

Creating a layout

The layout appears in an XML file. However, this layout isn't one of the standard layouts supported by Android Studio. Use the following steps to create the layout instead:

  1. Right-click the res folder and choose New⇒  Directory.

    You see a New Directory dialog box containing a single field in which you can enter a directory name.

  2. Type xml and click OK.

    Android Studio creates the new directory for you.

  3. Right-click the xml folder and choose New⇒  XML Resource.

    You see the New Resource File dialog box, shown in Figure 4-6.

    Screenshot of the New Resource File dialog box to create a new preference resource file, by typing preference_main in the File Name field and clicking OK.

    FIGURE 4-6: Create a new preference resource file.

  4. Type preference_main in the File Name field and click OK.

    Android Studio creates and opens the file for you. Notice that the root node automatically uses the name <PreferenceScreen>, which is precisely what you need for this example.

At this point, you can begin creating a layout. The easiest way to do this is to use the designer, just as you have for other layouts. The Palette will contain a list of acceptable preference types, as shown in Figure 4-7.

Screenshot of the Palette containing a list of acceptable preference types, to add just one CheckBoxPreference and a
EditTextPreference to the layout.

FIGURE 4-7: Each preference appears in a <Preference> element.

For this example, you start simple by adding just one CheckBoxPreference and a EditTextPreference to the layout, as shown in the figure. Here are the settings you use:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">

<CheckBoxPreference
android:defaultValue="true"
android:key="@string/ShowNameKey"
android:title="@string/ShowNameText"/>
<EditTextPreference
android:defaultValue="Default value"
android:key="@string/NewNameKey"
android:selectAllOnFocus="true"
android:title="@string/NewNameText"/>
</PreferenceScreen>

Tip You can add all sorts of elements to your preferences. If you look down the list of items in Attributes, you see that you can add a summary, create dependencies between preferences, use icons, and create a variety of visual elements, such as dividers between preferences.

Defining the preferences fragment class

You need a class derived from PreferenceFragmentCompat to support a preference fragment. This approach is different from the fragments shown in Chapter 3 of this minibook. The following code shows a basic implementation for a Preferences dialog box of the kind used for this app, which is relatively basic because it has only two components:

class NamePreferences: PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?, rootKey: String?) {

setPreferencesFromResource(
R.xml.preference_main, rootKey)

val showName: CheckBoxPreference? =
findPreference("ShowName")
val nameValue: EditTextPreference? =
findPreference("NewName")

val PrefChanges: Preference
.OnPreferenceChangeListener =
object : Preference.OnPreferenceChangeListener {
override fun onPreferenceChange(
preference: Preference?,
newValue: Any?): Boolean {
// Add Your Validation Code Here!
return true
}
}

showName?.onPreferenceChangeListener =
PrefChanges
nameValue?.onPreferenceChangeListener =
PrefChanges
}
}

You begin by overriding onCreatePreferences(). In fact, this is the only method within the NamePreferences class. The first task is to define the appearance of the Preferences dialog box using setPreferencesFromResource(). Notice that you must tell Android where to start looking for the layout, with the default being rootKey.

The next step is to gain access to the individual components within the Preferences dialog box. Notice that you must specify the component type: CheckBoxPreference or EditTextPreference in this case. When you have these objects, you can create a listener using Preference.OnPreferenceChangeListener. Overriding onPreferenceChange() allows you to intercept any preference changes and perform validation on them or work with them in other ways.

Remember The listener is now listening, but it isn't doing anything with either of the components. To connect the listener to the components, you must assign it to the component’s onPreferenceChangeListener. Creating the listener alerts you to changes while the Preferences dialog box is open; it doesn’t provide a means to access the components on MainActivity, which comes in the “Adding menu support” section, later in this chapter.

Saving and restoring the data

Android automatically calls onSaveInstanceState() when the app loses focus or is about to shut down. To make things easier, the code defines two constants for accessing the saved data:

private const val USER_NAME = "UserName"
private const val SHOW_USER = "ShowUser"

The following code implements the onSaveInstanceState() function to save the two settings on the Preferences dialog box:

override fun onSaveInstanceState(
outState: Bundle,
outPersistentState: PersistableBundle) {

super.onSaveInstanceState(outState,
outPersistentState)

outState.putCharSequence(USER_NAME, UserName.text)
outState.putInt(SHOW_USER, UserName.visibility)
}

Notice how the code calls the correct outState object function for saving the data. The first value is always a string containing the name of the key used to hold the data. Because it's so incredibly easy to mistype this information (and the compiler won’t catch it), using the constants is a better idea.

When you restart the app, you want to access this saved data to ensure that the interface appears as the user expects it to appear. You add this code to onCreate(), as shown in bold here:

override fun onCreate(savedInstanceState: Bundle?) {
… Default Code …

if (savedInstanceState != null) {
UserName.setText(
savedInstanceState.getCharSequence(USER_NAME))
UserName.visibility = savedInstanceState.getInt(
SHOW_USER)
}
}

When data is available (savedInstanceState will be null on the first execution of the app because you haven't saved any data), the code obtains the saved data and uses it to restore the user interface. In this case, it shows the username unless the user has decided not to display the username.

Adding menu support

You get an options menu with a Settings entry by default when you create the project. That entry doesn’t do anything — it just sits there looking pretty, as shown in Figure 4-8.

To display the Preferences dialog box, you must override the onOptionsItemSelected() function, as shown here:

override fun onOptionsItemSelected(item: MenuItem):
if (item.itemId == R.id.action_settings) {
supportFragmentManager
.beginTransaction()
.replace(R.id.preferences, NamePreferences())
.addToBackStack(null)
.commit()

supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}

A mobile screen displaying the Settings option that appears after clicking the ellipses.

FIGURE 4-8: The Settings option that appears after clicking the ellipses.

The last part of this code is added for you automatically by the IDE. The code in bold provides the functionality for displaying the Preferences dialog box. There are two steps:

  1. Display the Preferences dialog box fragment using a supportFragmentManager transaction, much the same as you do in Chapter 3 of this minibook. Note that the Preferences dialog box must appear on the backstack for the app to work correctly.
  2. Show the left pointing arrow, the Home As Up indicator, by calling supportActionBar?.setDisplayHomeAsUpEnabled(true), as shown in Figure 4-9.

The check box works as you might expect. To change the name, you click New Name to display the dialog box shown in Figure 4-10. The user changes the name and clicks OK to make it permanent or Cancel when there is a change of mind. The behavior shown here is provided as part of the EditTextPreference component; you don't have to provide it.

A mobile screen displaying the preference menu in the Preferences dialog box fragment on the screen.

FIGURE 4-9: Display the preference menu onscreen.

A mobile screen for changing the user name, by clicking New
Name to display the dialog box, with two options - Cancel and OK.

FIGURE 4-10: Changing the user name.

Updating the UI

You can try changing the name and performing other changes, but you’ll notice that the changes don’t appear immediately on the MainActivity display. That’s because they don’t get saved until the user clicks the left-pointing arrow in Figure 4-9 or the Back button. After the Preferences dialog box is dismissed, any changes take place. You obtain this functionality by overriding the onSupportNavigateUp() function, as shown here:

override fun onSupportNavigateUp(): Boolean {
val sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(this)
UserName.setText(sharedPreferences.getString(
"NewName", ""))
if (sharedPreferences.getBoolean("ShowName", true))
UserName.visibility = View.VISIBLE
else
UserName.visibility = View.INVISIBLE

if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
return true
}
return super.onSupportNavigateUp()
}

Remember To gain access to the components on the preferences dialog box, you begin by calling PreferenceManager.getDefaultSharedPreferences() to obtain access. You then access each of the values by using the key you set as part of creating the layout. This is a different approach from working with the fragments in Chapter 3, so you need to think through any required data access carefully. Because the ShowName component is a check box and the UserName.visibility property actually has three settings (VISIBLE, INVISIBLE, and GONE), you need an if statement to handle the change.

Notice that you must obtain any data you want before the call to supportFragmentManager.popBackStackImmediate(). After this call is made, the fragment no longer exists. This is also where you dismiss the left-pointing arrow by calling supportActionBar?.setDisplayHomeAsUpEnabled(false).

Working with MediaPlayer

You have a number of options when dealing with a media player in Android, but most of them will be overkill except for those situations for which the goal of the app is to create a flexible media player. If your goal is to play some background music for a game or to offer sound effects, you have an easier option than creating a full-fledged streaming solution.

Create a new app project named 03_04_03 using the same techniques as in Book 1, Chapter 3. Use an Empty Activity as a starting point, just as you have in the past. This example requires three buttons configured in a LinearLayout (Horizontal), like this:

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">

<Button
android:id="@+id/Play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="@string/PlayClick"
android:text="@string/PlayText"/>

<Button
android:id="@+id/Pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="false"
android:onClick="@string/PauseClick"
android:text="@string/PauseText"/>

<Button
android:id="@+id/Stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="false"
android:onClick="@string/StopClick"
android:text="@string/StopText"/>
</LinearLayout>

The designer will likely complain about some stylistic concerns, but you can safely ignore them. Now you need a resource to use. This example assumes that you're using a local resource, which means creating a new raw resource directory by right-clicking app es and choosing New⇒  Android Resource Directory. You see the dialog box shown in Figure 4-11, where you type raw (lowercase) in the Directory Name field and choose raw from the Resource Type drop-down list box.

Screenshot of the New Resource Directory dialog box, where you type raw (lowercase) in the Directory Name field and choose raw from the Resource Type drop-down list box.

FIGURE 4-11: Creating a directory to hold the music source.

Next, copy a music or other sound file. Right-click the raw directory and choose Paste from the context menu. You see a Copy dialog box, in which you choose what to call the file when you move it into the raw folder. The filename you choose must not contain any spaces and must be lowercase. Otherwise, you're free to use any name you like (without changing the file extension). The file will appear in the raw directory. If you later find that you can’t access this file as a resource, recheck the filename; simple names work best. The example uses citysunshine.mp3 as the example file. You can find public domain sound files at https://freepd.com/ (and many other sources).

For once, you don't need to jump through any insane hoops to get the smallest feature of Android working. The code is almost too easy when compared to a few of the other book examples. Here’s all you need to implement all three buttons:

private var mediaPlayer: MediaPlayer ?= null

fun onPlayClick (view: View) {
mediaPlayer = MediaPlayer.create(
this, R.raw.citysunshine)
mediaPlayer?.start()
Play.isClickable = false
Pause.isClickable = true
Stop.isClickable = true
}

fun onPauseClick (view: View) {
mediaPlayer?.pause()
Play.isClickable = true
Pause.isClickable = false
Stop.isClickable = true
}

fun onStopClick (view: View) {
mediaPlayer?.stop()
Play.isClickable = true
Pause.isClickable = false
Stop.isClickable = false
}

You begin by creating mediaPlayer. It’s important to declare the object outside onPlayClick() because you need to access the same object from all three button handlers. The call to MediaPlayer.create() requires just the app context and the name of the resource you want to play. Because you can't be certain that the file exists (or might be corrupted in some way) and mediaPlayer might be null, you must use mediaPlayer?.start() to start playing the file. To pause and stop the media stream, you call pause() and stop(), as you might expect.

The remainder of the code focuses on ensuring that the user can click only acceptable buttons. You can add visual cues, too. The point is to disable buttons that aren't acceptable for use at a particular time.

Adding Camera Support Using CameraX

Working with any device can be difficult because the device has its own Supervisory Control and Data Acquisition (SCADA) interface. The interface varies by vendor, and you can’t ever be sure that the vendor won’t decide to change things when you least expect it. You may see the camera (and other devices) as built-in and a cohesive part of the underlying computer, but really, it’s not. So, your app has to do things like send commands to start the camera, control its inner circuitry, stop it, activate the flash, and a wealth of other considerations too numerous to list. This complexity is why operating systems rely on device drivers (low-level libraries that provide a somewhat common interface to your app) to make interacting with these devices easier because only someone with low-level knowledge can actually control them effectively, which is where CameraX comes into play. Even with a device driver to ease the pain of dealing with all those arcane commands, development is still difficult. CameraX (https://developer.android.com/reference/kotlin/androidx/camera/core/CameraX) makes things easier still by providing just a single interface for you to deal with and a lot of automation to perform some tasks for you.

From previous examples in this minibook, you know that API features control just how effective Android is at communicating with the underlying hardware for you. Android actually supports two camera APIs: Camera 1 and Camera 2. Even though the Camera 1 API is deprecated, many developers still use it because it’s simpler and easier to use than Camera 2. You can read a comparison of Camera 1 and Camera 2 at https://infinum.com/the-capsized-eight/conquering-android-camera-api. This section focuses on Camera 2 because that’s what CameraX automates for you. The theory is that using CameraX removes some of the concerns developers have had about working with Camera 2 directly.

However, CameraX does a lot more than alleviate some Camera 2 issues. For example, when working with either of the camera APIs, you soon discover that your math skills had better be nearly perfect and that you need to know about topics like matrix manipulation; otherwise, getting a good picture is a lost cause. In fact, even when you get the API to work, it often provides inconsistent results depending on which camera your device contains. So it’s good to know what sorts of things CameraX will do for you that the APIs don’t, as follows:

  • Provides compatibility with about 90 percent of the devices on the market when using Android Lollipop (API 21) or above.
  • Fixes some of the consistency issues that developers face when working with the camera APIs, such as uncertainty over how a list of camera resolutions is sorted after making an API call.
  • Reduces the complexity of using advanced camera features such as Portrait, Highly Dynamic Range (HDR), Night Mode, and so on.
  • Introduces the concept of USECASES to encapsulate specific tasks in automation. You can now focus on specific tasks, such as Preview, Image Analysis, and Image Capture.
  • Builds a lifecycle orientation into camera use so that you gain access to event-handling features such as onResume() and onPause() (see the “Considering the activity lifecycle” and “Considering the fragment lifecycle” sections of Chapter 3 of this minibook for an overview of how lifecycle management works).
  • Fixes some known camera API issues, including:
    • Front/back camera switch crashes
    • Optimizes camera closures
    • Orientation issues
    • Flash not firing correctly or at all

As with other examples in this chapter, you follow a basic process when creating a CameraX application, starting with the Gradle dependencies section additions (note that the Implementation code must appear on a single line even though it appears on two lines in the book):

def camerax_version = "1.0.0-alpha09"
implementation
"androidx.camera:camera-core:$camerax_version"
implementation
"androidx.camera:camera-camera2:$camerax_version"

Warning In case you're wondering about the version number for CameraX, be aware that there isn’t a beta release, much less a full release, as of this writing. Working with alpha-level code means that you should expect inconsistencies, instability, bad documentation, and the like. That’s why many articles about using CameraX begin with complaints about these very issues.

Because you’re using an external device, you need permission, which means adding an entry to AndroidManifest.xml like this (along with the required code when you actually use the camera in your code):

<uses-permission android:name="android.permission.CAMERA"/>

Android considers this a dangerous level of permission (see the “Configuring permissions in AndroidManifest.xml” section, earlier in this chapter), so it will ask the user for permission before you do anything with the camera. Of course, this means adding code to ensure that your app fails gracefully (it doesn’t simply freeze up and possibly lose data) if the user doesn’t grant permission.

The next step is to create a layout for your app, which works much like the fragment-based apps used in this chapter and Chapter 3 of this minibook as well. The difference is that you need to use a TextureView (https://developer.android.com/reference/android/view/TextureView) to make the camera interface work properly. You find the component in the Widgets folder of the Palette. You use a TextureView because you need to be able to post to it in onCreate() so that the camera is active when the app starts, like this:

texture.post { startCamera() }

The startCamera() function performs these basic steps:

  1. Get the camera metrics because they're all different:

    var lensFacing = CameraX.LensFacing.BACK
    val metrics = DisplayMetrics().also {
    texture.display.getRealMetrics(it) }
    val screenSize = Size(metrics.widthPixels,
    metrics.heightPixels)
    val screenAspectRatio = Rational(metrics.widthPixels,
    metrics.heightPixels)

  2. Configure a preview using PreviewConfig.Builder().apply that relies on the camera metrics and user requirements.
  3. Define the preview characteristics using Preview(previewConfig), which means creating a transform to adjust the camera input to the user's screen, like this:

    fun updateTransform() {
    val matrix = Matrix()
    val centerX = texture.width / 2f
    val centerY = texture.height / 2f

    val rotationDegrees = when
    (texture.display.rotation) {
    Surface.ROTATION_0 -> 0
    Surface.ROTATION_90 -> 90
    Surface.ROTATION_180 -> 180
    Surface.ROTATION_270 -> 270
    else -> return
    }
    matrix.postRotate(-rotationDegrees.toFloat(),
    centerX, centerY)
    texture.setTransform(matrix)
    }

  4. Set up a preview listener using setOnPreviewOutputUpdateListener.
  5. Configure an image capture use case with ImageCaptureConfig.Builder().apply.
  6. Build the image capture use case using ImageCapture(imageCaptureConfig) and implementing a setOnClickListener for the button that the user will click to take the image.

These steps get you started. You could take a basic picture using them, but most users will want more. This means implementing various features and various modes, such as the Bokeh mode provided by the iPhone camera.

Sharing with Others

The various fragment examples so far should lead you to the conclusion that an Android app can be extremely flexible. If you come from a desktop-application background, you may be used to the idea that the application is self-contained. Web applications are more like Android applications in that you can embed content from other locations into a web application to create a smorgasbord of data for the user. However, Android kicks the whole concept of sharing up a few notches by making sharing more about getting what you need wherever the data may appear and less about using specific data sources as you would with a web application.

Performing simple share actions with other apps

Android users want to interact with any and every sort of data available, but your app may not offer the functionality to deal with that data, and you may not want to invest time reinventing code that someone else has already created. The idea of using another app's functionality to address your user’s need can allow your app to do a lot more than it ordinarily could with less coding on your part. The following sections tell you about the two elements of this process: sending data to other apps and receiving data from other apps.

Sending data to other apps

You see in Chapter 3 of this minibook how to get activities and fragments to interact and exchange data using intents. When you work with a process that involves having the user engaged in a well-defined task, using the intent approach works best. For example, if part of performing a task is to view a document, but your app doesn’t support that document type, you can determine whether another app has the required functionality.

However, when the focus is on freeform activities, such as dealing with texts or other kinds of instantaneous communication, no process may be in place to work with the data. In such a case, you need to use an Android Sharesheet instead. A Sharesheet not only works on the local device but also can interact with apps on other devices, such as sending a URL to a friend.

A Sharesheet is a centrally managed repository of apps that can handle specific kinds of data interaction. You don’t manage the Sharesheet. You simply provide the means for a user to select a target for the data based on the target’s app and user activity that is maintained by the system.

To start the process, you create an intent and set its action to Intent.ACTION_SEND. The example in the “Starting Another Activity” section of Book 1, Chapter 6 shows how to perform this task and describes how an intent works. After you create the intent, you call Intent.createChooser() and pass it your Intent object. The result is an Android Sharesheet that the user can use to choose a target for some data. The article at https://developer.android.com/training/sharing/send offers additional insights as to how this process works.

Receiving data from other apps

To receive data from another app, you can use an intent-filter tag in the app manifest, as described in the “Of tags and elements” section of Book 2, Chapter 7. However, you also have the option to create a ChooserTarget object that will show up in an Android Sharesheet. If you use Android 10 (API level 29) exclusively, you also have the option to use Sharing Shortcuts. Unlike a ChooserTarget object, a Sharing Shortcut can target a specific person, rather than an app as a whole.

Using Slices

A slice is a kind of user interface fragment that results from a data search of some type. Most slices rely on content from the Google Search app or from Google Assistant. A user makes a request for information, and you simply find it and display it in your app. As far as the user is concerned, the data environment is seamless, so it's not like embedding in a web app, which lets you sometimes tell that the information comes from another source. However, the downside of using Slices templates is that you must have a minimum of Android 4.4 (API level 19) to use it.

With slices, you can mix content you get from any source you search with standard Android controls like toggles and sliders. The example apps that Android provides shows how to set up things like an itinerary, with live content from the various locations you plan to visit. Another example shows a work itinerary that updates driving time and other essential information based on your current location.

Slices is a work in progress, according to the documentation at https://developer.android.com/guide/slices. However, you can get started using Slices templates by using the techniques found at https://developer.android.com/guide/slices/getting-started. Make sure to address the installation requirements and the Gradle dependencies additions to make Slices active in Android Studio.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.170.81.33