Chapter 4
IN THIS CHAPTER
Interacting with users through notifications
Ensuring that data remains safe and user accessible
Working with various kinds of data
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.
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.
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:
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.
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.
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:
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.
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.
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.
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.
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:
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 messageCATEGORY_EMAIL
: Any incoming asynchronous message, usually in bulk form, such as an emailCATEGORY_EVENT
: Any sort of an event, such as one found on a calendarCATEGORY_PROMO
: A promotion or advertisementCATEGORY_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 goalCATEGORY_SOCIAL
: Any type of social network, resource sharing, or information update communicationCATEGORY_ERROR
: A notice that something bad has happened to an app, background operation, authentication, device, or something else equally regrettable in its effectsCATEGORY_TRANSPORT
: Any type of notification (not necessarily bad) related to the media transport control for playback 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)Some notifications are more important than others, and notifications come with these importance-level restrictions:
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 |
|
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. |
|
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. |
|
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). |
|
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. |
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:
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:
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
).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.
You create a notification to tell the user about something special. Android uses a four-step process to accomplish this task:
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
.
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.
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")
}
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())
}
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.
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.
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:
https://developer.android.com/training/notify-user/custom-notification
describes how to create a custom template.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.
No matter what you want permission to do, you follow the same set of steps to obtain and verify the permission:
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.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.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:
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.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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.
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'
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:
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.
Type xml and click OK.
Android Studio creates the new directory for you.
Right-click the xml
folder and choose New⇒ XML Resource.
You see the New Resource File dialog box, shown in Figure 4-6.
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.
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>
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.
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.
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)
}
}
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:
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.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.
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()
}
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)
.
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.
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.
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:
USECASES
to encapsulate specific tasks in automation. You can now focus on specific tasks, such as Preview, Image Analysis, and Image Capture.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).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"
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:
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)
PreviewConfig.Builder().apply
that relies on the camera metrics and user requirements.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)
}
setOnPreviewOutputUpdateListener
.ImageCaptureConfig.Builder().apply
.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.
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.
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.
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.
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.
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.
35.170.81.33