This chapter is about design guidelines for writing imaginative and useful Android applications. Several recipes describe specific aspects of successful design. This section will list some others.
One purpose of this chapter is to explain the benefits of developing native Java Android applications over other methods of delivering rich content on mobile devices.
Requirements of a native handset application
There are a number of key requirements for successfully delivering any mobile handset application, regardless of the platform onto which it will be deployed:
The application should be easy to install, remove, and update on a device.
It should address the user’s needs in a compelling, unique, and elegant way.
It should be feature-rich while remaining usable by both novice and expert users.
It should be familiar to users who have accessed the same information through other routes, such as a website.
Key areas of functionality should be readily accessible.
It should have a common look and feel with other native applications on the handset, conforming to the target platform’s standards and style guidelines.
It should be stable, scalable, usable, and responsive.
It should use the platform’s capabilities tastefully, when it makes the user’s experience more compelling.
Android application design
Colin Wilcox
The Android application we will design in this chapter will exploit the features and functions unique to the Android OS platform. In general, the application will be an Activity-based solution allowing independent and controlled access to data on a screen-by-screen basis. This approach helps to localize potential errors and allows sections of the flow to be readily replaced or enhanced independently of the rest of the application.
Navigation will use a similar approach to that of the Apple iPhone solution in that all key areas of functionality will be accessed from a single navigation bar control. The navigation bar will be accessible from anywhere within the application, allowing the user to freely move around the application.
The Android solution will exploit features inherent to Android devices, supporting the devices’ touch-screen features, the hardware button that allows users to switch the application to the background, and application switching capability.
Android provides the ability to jump back into an application at the point where it was switched out. This will be supported, when possible, within this design.
The application will use only standard Android user interface controls to make it as portable as possible. The use of themes or custom controls is outside the scope of this chapter.
The application will be designed such that it interfaces to a thin layer of RESTful web services that provide data in a JSON format. This interface will be the same as the one used by the Apple iPhone, as well as applications written for other platforms.
The application will adopt the Android style and design guidelines wherever possible so that it fits in with other Android applications on the device.
Data that is local to each view will be saved when the view is exited and automatically restored with the corresponding user interface controls repopulated when the view is next loaded.
A number of important device characteristics should be considered, as discussed in the following subsections.
Screen size and density
In order to categorize devices by their screen type, Android defines two characteristics for each device: screen size (the physical dimensions of the screen) and screen density (the physical density of the pixels on the screen, or dpi [dots per inch]). To simplify all the different types of screen configurations, the Android system generalizes them into select groups that make them easier to target.
The designer should take into account the most appropriate choices for screen size and screen density when designing the application.
By default, an application is compatible with all screen sizes and densities, because the Android system makes the appropriate adjustments to the UI layout and image resources. However, you should create specialized layouts for certain screen sizes and provide specialized images for certain densities, by using alternative layout resources and by declaring in your manifest exactly which screen sizes your application supports.
Input configurations
Many devices provide a different type of user input mechanism, such as a hardware keyboard, a trackball, or a five-way navigation pad. If your application requires a particular kind of input hardware, you must declare it in the AndroidManifest.xml file, and be aware that the Google Play Store will not display your app on devices that lack this feature. However, it is rare for an application to require a certain input configuration.
Device features
There are many hardware and software features that may or may not exist on a given Android-powered device, such as a camera, a light sensor, Bluetooth capability, a certain version of OpenGL, or the fidelity of the touch screen. You should never assume that a certain feature is available on all Android-powered devices (other than the availability of the standard Android library).
A sophisticated Android application will use both types of menus provided by the Android framework, depending on the circumstances:
Options menus contain primary functionality that applies globally to the current Activity or starts a related Activity. An options menu is typically invoked by a user pressing a hard button, often labeled Menu, or a soft menu button on an Action Bar (a vertical stack of three dots).
Context menus contain secondary functionality for the currently selected item. A context menu is typically invoked by a user performing a long-press (press and hold) on an item. Like on the options menu, the selected operation can run in either the current or another Activity.
A context menu is for any commands that apply to the current selection.
The commands on the context menu that appear when you long-press on an item should be duplicated in the Activity you get to by a normal press on that item.
As very general guidance:
Place the most frequently used operations first in the menu.
Only the most important commands should appear as buttons on the screen; delegate the rest to the menu.
Consider moving menu items to the action bar if your application uses one.
The system will automatically lay out the menus and provide standard ways for users to access them, ensuring that the application will conform to the Android user interface guidelines. In this sense, menus are familiar and dependable ways for users to access functionality across all applications.
Our Android application will make extensive use of Google’s Intent mechanism for passing data between Activity objects. Intents not only are used to pass data between views within a single application, but also allow data, or requests, to be passed to external modules. As such, much functionality can be adopted by the Android application by embedded functionality from other applications invoked by Intent calls. This reduces the development process and maintains the common look and feel and functionality behavior across all applications.
Data feeds and feed formats
It is not a good idea to interface directly to any third-party data source; for example, it would be a bad idea to use a Type 3 JDBC driver in your mobile application to talk directly to a database on your server. The normal approach would be to mediate the data, from several sources in potentially multiple data formats, through middleware, which then passes data to an application through a series of RESTful web service APIs in the form of JSON data streams.
Typically, data is provided in such formats as XML, SOAP, or some other XML-derived representation. Representations such as SOAP are heavyweight, and as such, transferring data from the backend servers in this format increases development time significantly as the responsibility of converting this data into something more manageable falls on either the handset application or an object on the middleware server.
Mitigating the source data through a middleware server also helps to break the dependency between the application and the data. Such a dependency has the disadvantage that if, for some reason, the nature of the data changes or the data cannot be retrieved, the application may be broken and become unusable, and such changes may require the application to be republished. Mitigating the data on a middleware server ensures that the application will continue to work, albeit possibly in a limited fashion, regardless of whether the source data exists. The link between the application and the mitigated data will remain.
2.1 Exception Handling
Ian Darwin
Problem
Java has a well-defined exception handling mechanism, but it takes some time to learn to use it effectively without frustrating either users or tech support people.
Solution
Java offers an exception hierarchy that provides considerable flexibility when used correctly. Android offers several mechanisms, including dialogs and toasts, for notifying the user of error conditions. The Android developer should become acquainted with these mechanisms and learn to use them effectively.
Discussion
Java has had two categories of exceptions (actually of Exception’s parent, Throwable) since it was introduced: checked and unchecked. In Java Standard Edition, apparently the intention was to force the programmer to face the fact that, while certain things could be detected at compile time, others could not. For example, if you were installing a desktop application on a large number of PCs, it’s likely that the disk on some of those PCs would be near capacity, and trying to save data on them could fail; meanwhile, on other PCs some file that the application depended upon might have gone missing, not due to programmer error but to user error, filesystem happenstance, gerbils chewing on the cables, or whatever. So the category of IOException was created as a “checked exception,” meaning that the programmer would have to check for it, either by having a try-catch clause inside the file-using method or by having a throws clause on the method definition. The general rule, which all well-trained Java developers memorize, is the following:
Throwable is the root of the throwable hierarchy. Exception, and all of its subclasses other than RuntimeException or any subclass thereof, is checked. All else is unchecked.
This means that Error and all of its subclasses are unchecked (see Figure 2-1). If you get a VMError, for example, it means there’s a bug in the runtime. There’s nothing you can do about this as an application programmer. RuntimeException subclasses include things like the excessively long-named ArrayIndexOutOfBoundsException; this and friends are unchecked because it is your responsibility to catch these exceptions at development time, by testing for them (see Chapter 3).
Where to catch exceptions
The (over)use of checked exceptions led a lot of early Java developers to write code that was sprinkled with try-catch blocks, partly because the use of the throws clause was not emphasized early enough in some training programs and books. As Java itself has moved more to enterprise work, and newer frameworks such as Spring, Hibernate, and JPA have come along and are emphasizing the use of unchecked exceptions, this early position has shifted. It is now generally accepted that you want to catch exceptions as close to the user as possible. Code that is meant for reuse—in libraries or even in multiple applications—should not try to do error handling. What it can do is what’s called exception translation; that is, turning a technology-specific (and usually checked) exception into a generic, unchecked exception. Example 2-1 shows the basic pattern.
Example 2-1. Exception translation
publicclassExceptionTranslation{publicStringreadTheFile(Stringf){try(BufferedReaderis=newBufferedReader(newFileReader(f))){Stringline=is.readLine();returnline;}catch(FileNotFoundExceptionfnf){thrownewRuntimeException("Could not open file "+f,fnf);}catch(IOExceptionex){thrownewRuntimeException("Problem reading file "+f,ex);}}}
Note that prior to Java 7, you’d have had to write an explicit finally clause to close the file:
}finally{if(is!=null){try{is.close();}catch(IOExceptiongrr){thrownewRuntimeException("Error on close of "+f,grr);}}}}
Note how the use of checked exceptions clutters even that code: it is virtually impossible for the is.close() to fail, but since you want to have it in a finally block (to ensure that it gets tried if the file was opened but then something went wrong), you have to have an additional try-catch around it. So, checked exceptions are (more often than not) an annoyance, should be avoided in new APIs, and should be paved over with unchecked exceptions when using code that requires them.
There is an opposing view, espoused by the official Oracle website and others. In a comment on
the website from which this book was produced,
reader Al Sutton points out the following:
Checked exceptions exist to force developers to acknowledge that an error condition can occur and that they have thought about how they want to deal with it. In many cases there may be little that can be done beyond logging and recovery, but it is still an acknowledgment by the developer that they have considered what should happen with this type of error. The example shown … stops callers of the method from differentiating between when a file doesn’t exist (and thus may need to be re-fetched), and when there is a problem reading the file (and thus the file exists but is unreadable), which are two different types of error conditions.
Android, wishing to be faithful to the Java API, has a number of these checked exceptions (including the ones shown in the example), so they should be treated the same way.
What to do with exceptions
Exceptions should almost always be reported. When I see code that catches exceptions and does nothing at all about them, I despair. They should, however, be reported only once (do not both log and translate/rethrow!). The point of all normal exceptions is to indicate, as the name implies, an exceptional condition. Since on an Android device there is no system administrator or console operator, exceptional conditions need to be reported to the user.
You should think about whether to report exceptions via a dialog or a toast. The exception handling situation on a mobile device is different from that on a desktop computer. The user may be driving a car or operating other machinery, interacting with people, and so on, so you should not assume you have her full attention.
I know that most examples, even in this book, use a toast, because it involves less coding than a dialog. But remember that a toast will only appear on the screen for a few seconds; blink and you may miss it. If the user needs to do something to correct the problem, you should use a dialog.
Toasts simply pop up and then obliviate. Dialogs require the user to acknowledge an exceptional condition, and either do, or give the app permission to do, something that might cost money (such as turning on internet access in order to run an application that needs to download map tiles).
Note
Use toasts to “pop up” unimportant information; use dialogs to display important information and to obtain confirmation.
In Android 6 and later, you must check permissions at runtime in addition to specifying them in the manifest.
Solution
“Dangerous” resources are those that could affect the user’s stored information, or privacy, etc.
To access resources protected by “dangerous” permissions you must:
Check if the user has already granted permission before accessing a resource.
Explicitly request permissions from the user if the permissions have not previously been granted.
Have an alternate course of action so the application does not crash if the permission is not granted.
Discussion
Before accessing a resource that requires permission, you must first
check if the user has already granted permission.
To do this, call the Activity method checkSelfPermission(permission). It will return either
PERMISSION_GRANTED or PERMISSION_DENIED:
if(ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)==PackageManager.PERMISSION_GRANTED){// If you get here then you have the permission and can do some work}else{// See below}
If the preceding check indicates that the permission has not been granted, you must explicitly request
it by calling the Activity method requestPermissions():
As this will interact with the user, it is an asynchronous request.
You must override the Activity method onRequestPermissionsResult() to get notified of the response:
// Unique request code for the particular permissions requestprivatestaticintREQUEST_EXTERNAL_STORAGE=1;...// Request the permission from the userActivityCompat.requestPermissions(this,newString[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},REQUEST_EXTERNAL_STORAGE);// Callback handler for the eventual response@OverridepublicvoidonRequestPermissionsResult(intrequestCode,String[]permissions,int[]grantResults){booleangranted=true;if(requestCode==REQUEST_EXTERNAL_STORAGE){// Received permission result for external storage permission.Log.i(TAG,"Got response for external storage permission request.");// Check if all the permissions have been grantedif(grantResults.length>0){for(intresult:grantResults){if(result!=PackageManager.PERMISSION_GRANTED){granted=false;}}}else{granted=false;}}...// If granted is true: carry on and perform the action. Calling// checkSelfPermission() will now return PackageManager.PERMISSION_GRANTED
It is usually a good idea to provide the user with information as to why the permissions are required.
To do this you call the Activity method boolean shouldShowRequestPermissionRationale(String permission).
If the user has previously refused to grant the permissions this method will return true,
giving you the opportunity to display extra information as to why they should be granted:
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)){// Provide additional info if the permission was not granted// and the user would benefit from additional// context for the use of the permissionLog.i(TAG,"Displaying permission rationale to provide additional context.");Snackbar.make(mLayout,R.string.external_storage_rationale,Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,newView.OnClickListener(){@OverridepublicvoidonClick(Viewview){ActivityCompat.requestPermissions(MainActivity.this,newString[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE},REQUEST_EXTERNAL_STORAGE);}}).show();
This uses a Snackbar (see Recipe 7.1) to display the rationale, until the user clicks the Snackbar to dismiss it.
2.3 Accessing Android’s Application Object as a “Singleton”
Adrian Cowham
Problem
You need to access “global” data from within your Android app.
Solution
The best solution is to subclass android.app.Application and treat it as a singleton with static accessors. Every Android app is guaranteed to have exactly one android.app.Application instance for the lifetime of the app. If you choose to subclass android.app.Application, Android will create an instance of your class and invoke the android.app.Application life-cycle methods on it. Because there’s nothing preventing you from creating another instance of your subclassed android.app.Application, it isn’t a genuine singleton, but it’s close enough.
Having global access to such objects as session handlers, web service gateways, or anything that your application only needs a single instance of will dramatically simplify your code. Sometimes these objects can be implemented as singletons, and sometimes they cannot because they require a Context instance for proper initialization. In either case, it’s still valuable to add static accessors to your subclassed android.app.Application instance so that you can consolidate all globally accessible data in one place, have guaranteed access to a Context instance, and easily write “correct” singleton code without having to worry about synchronization.
Discussion
When writing your Android app you may find it necessary to share data and services across multiple Activities. For example, if your app has session data, such as the identity of the currently logged-in user, you will likely want to expose this information. When developing on the Android platform, the pattern for solving this problem is to have your android.app.Application instance own all global data, and then treat your Application instance as a singleton with static accessors to the various data and services.
When writing an Android app you’re guaranteed to only have one instance of the android.app.Application class, so it’s safe (and recommended by the Google Android team) to treat it as a singleton. That is, you can safely add a static getInstance() method to your Application implementation. Example 2-2 provides an example.
Example 2-2. The Application implementation
publicclassAndroidApplicationextendsApplication{privatestaticAndroidApplicationsInstance;privateSessionHandlersessionHandler;// Generic your-application handlerpublicstaticAndroidApplicationgetInstance(){returnsInstance;}publicSessionHandlergetSessionHandler()returnsessionHandler;}@OverridepublicvoidonCreate(){super.onCreate();sInstance=this;sInstance.initializeInstance();}protectedvoidinitializeInstance(){// Do all your initialization heresessionHandler=newSessionHandler(this.getSharedPreferences("PREFS_PRIVATE",Context.MODE_PRIVATE));}/** This is a stand-in for some application-specific session handler; * would normally be a self-contained public class. */privateclassSessionHandler{SharedPreferencessp;SessionHandler(SharedPreferencessp){this.sp=sp;}}}
This isn’t the classical singleton implementation, but given the constraints of the Android framework it’s the closest thing we have; it’s safe, and it works.
The notion of the “session handler” is that it keeps track of per-user information such
as name and perhaps password, or any other relevant information, across different Activities
and the same Activity even if it gets destroyed and re-created. Our SessionHandler
class is a placeholder for you to compose such a handler, using whatever information you
need to maintain across Activities!
Using this technique in this app has simplified and cleaned up the implementation. Also, it has made it much easier to develop tests. Using this technique in conjunction with the Robolectric testing framework (see Recipe 3.5), you can mock out the entire execution environment in a straightforward fashion.
Also, don’t forget to add the application class’s android:"name" declaration to the existing
application element in your AndroidManifest.xml file:
When the user rotates the device, Android will normally destroy and re-create the current Activity. You want to keep some data across this cycle, but all the fields in your Activity are lost during it.
Solution
There are several approaches. If all your data comprises primitive types, consists of Strings, or is Serializable, you can save it in onSaveInstanceState() in the Bundle that is passed in.
Another solution lets you return a single arbitrary object. You need only override onRetainNonConfigurationInstance() in your Activity to save some values, call getLastNonConfigurationInstance() near the end of your onCreate() method to see if there is a previously saved value, and, if so, assign your fields accordingly.
The getLastNonConfigurationInstance() method’s return type is Object, so you can return any value you want from it. You might want to create a Map or write an inner class in which to store the values, but it’s often easier just to pass a reference to the current Activity, for example, using this:
publicclassMyActivityextendsActivity{.../** Returns arbitrary single token object to keep alive across * the destruction and re-creation of the entire Enterprise. */@OverridepublicObjectonRetainNonConfigurationInstance(){returnthis;}
The preceding method will be called when Android destroys your main Activity. Suppose you wanted to keep a reference to another object that was being updated by a running service, which is referred to by a field in your Activity. There might also be a Boolean to indicate whether the service is active. In the preceding code, we return a reference to the Activity from which all of its fields can be accessed (even private fields, since the outgoing and incoming Activity objects are of the same class). In my geotracking app JPSTrack, for example, I have a FileSaver class that accepts data from the location service; I want it to keep getting the location, and saving it to disk, in spite of rotations, rather than having to restart it every time the screen rotates. Rotation is unlikely if the device is anchored in a car dash mount (we hope), but quite likely if a passenger, or a pedestrian, is taking pictures or typing notes while geotracking.
After Android creates the new instance, it calls onCreate() to notify the new instance that it has been created. In onCreate() you typically do constructor-like actions such as initializing fields and assigning event listeners. You still need to do those, so leave them alone. Near the end of onCreate(), however, you will add some code to get the old instance, if there is one, and get some of the important fields from it. The code should look something like Example 2-3.
Example 2-3. The onCreate() method
@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);saving=false;paused=false;// Lots of other initializations...// Now see if we just got interrupted by, e.g., rotationMainold=(Main)getLastNonConfigurationInstance();if(old!=null){saving=old.saving;paused=old.paused;// This is the most important line: keep saving to same file!fileSaver=old.fileSaver;if(saving){fileNameLabel.setText(fileSaver.getFileName());}return;}// I/O helperfileSaver=newGPSFileSaver(...);}
The fileSaver object is the big one, the one we want to keep running and not re-create every time. If we don’t have an old instance, we create the fileSaver only at the very end of onCreate(), since otherwise we’d be creating a new one just to replace it with the old one, which is (at the least) bad for performance. When the onCreate() method finishes, we hold no reference to the old instance, so it should be eligible for Java garbage collection. The net result is that the Activity appears to keep running nicely across screen rotations, despite the re-creation.
An alternative possibility is to set android:configChanges="orientation" in your AndroidManifest.xml. This approach prevents the Activity from being destroyed and re-created, but typically also prevents the application from displaying correctly in landscape mode, and is officially regarded as not good practice—see
the following reference.
You can download the source code for this example from GitHub. Note that if you want it to compile, you will also need the jpstrack project, from the same GitHub account.
2.5 Monitoring the Battery Level of an Android Device
Pratik Rupwal
Problem
You want to detect the battery level on an Android device so that you can notify the user when the battery level goes below a certain threshold, thereby avoiding unexpected surprises.
Solution
A broadcast receiver that receives the broadcast message sent when the battery status changes can identify the battery level and can issue alerts to users.
Discussion
Sometimes we need to show an alert to the user when the battery level of an Android device goes below a certain limit. The code in Example 2-4 sets the broadcast message to be sent whenever the battery level changes and creates a broadcast receiver to receive the broadcast message, which can alert the user when the battery gets discharged below a certain level.
Example 2-4. The main Activity
publicclassMainActivityextendsActivity{/** Called when the Activity is first created. */@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);/** This registers the receiver for a broadcast message to be sent * to when the battery level is changed. */this.registerReceiver(this.myBatteryReceiver,newIntentFilter(Intent.ACTION_BATTERY_CHANGED));/** Intent.ACTION_BATTERY_CHANGED can be replaced with * Intent.ACTION_BATTERY_LOW for receiving * a message only when battery level is low rather than sending * a broadcast message every time battery level changes. * There is also ACTION_BATTERY_OK for when the battery * has been charged a certain amount above the level that * would trigger the low condition. */}privateBroadcastReceivermyBatteryReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextctx,Intentintent){// bLevel is battery percent-full as an integerintbLevel=intent.getIntExtra("level",0);Log.i("BatteryMon","Level now "+bLevel);}};}
The ACTION_BATTERY_LOW and ACTION_BATTERY_OK levels are not documented,
and are settable only by rebuilding the operating system, but they may
be around 10 and 15, or 15 and 20, respectively.
2.6 Creating Splash Screens in Android
Rachee Singh and Ian Darwin
Problem
You want to create a splash screen that will appear while an application is loading.
Solution
You can construct a splash screen as an Activity or as a dialog. Since its purpose is accomplished within a few seconds, it can be dismissed after a short time interval has elapsed or upon the click of a button in the splash screen.
Discussion
The splash screen was invented in the PC era, initially as a cover-up for slow GUI construction when PCs were slow. Vendors have kept them for branding purposes. But in the mobile world, where the longest app start-up time is probably less than a second, people are starting to recognize that splash screens have become somewhat anachronistic. When I (Ian Darwin) worked at
eHealth Innovation, we recognized this by making the splash screen for our BANT application disappear after just one second. The question arises whether we still need splash screens at all. With most mobile apps, the name and logo appear in the app launcher, and on lots of other screens within the app. Is it time to make the splash screen disappear altogether?
The answer to that question is left up to you and your organization.
For completeness, here are two methods of handling the application splash screen.
The first version uses an Activity that is dedicated to displaying the splash screen. The splash screen displays for two seconds or until the user presses the Menu key, and then the main Activity of the application appears. First we use a thread to wait for a fixed number of seconds, and then we use an Intent to start the real main Activity. The one downside to this method is that your “main” Activity in your AndroidManifest.xml file is the splash Activity, not your real main Activity. Example 2-5 shows the splash Activity.
One additional requirement is to put the attribute android:noHistory="true" on the splash Activity in your AndroidManifest.xml file so that this Activity will not appear in the history stack, meaning if the user uses the Back button from the main app he will go to the expected Home screen, not back into your splash screen (see Figure 2-2).
Two seconds later, this Activity leads to the next Activity, which is the standard “Hello, World” Android Activity, as a proxy for your application’s main Activity (see Figure 2-3).
In the second version (Example 2-7), the splash screen displays until the Menu key on the Android device is pressed, then the main Activity of the application appears. For this, we add a Java class that displays the splash screen. We check for the pressing of the Menu key by checking the KeyCode and then finishing the Activity (see Example 2-7).
The layout of the splash Activity, splash.xml, is unchanged from the earlier version.
As before, after the button press this Activity leads to the next Activity, which represents the main Activity.
The other major method involves use of a dialog, started from the onCreate() method in your main method. This has a number of advantages, including a simpler Activity stack and the fact that you don’t need an extra Activity that’s only used for the first few seconds. The disadvantage is that it takes a bit more code, as you can see in Example 2-8.
Example 2-8. The splash dialog
publicclassSplashDialogextendsActivity{privateDialogsplashDialog;/** Called when the Activity is first created. */@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);StateSaverdata=(StateSaver)getLastNonConfigurationInstance();if(data!=null){// "All this has happened before"if(data.showSplashScreen){// And we didn't already finishshowSplashScreen();}setContentView(R.layout.main);// Do any UI rebuilding here using saved state}else{showSplashScreen();setContentView(R.layout.main);// Start any heavy-duty loading here, but on its own thread}}
The basic idea is to display the splash dialog at application startup, but also to redisplay it if you get, for example, an orientation change while the splash screen is running, and to be careful to remove it at the correct time if the user backs out or if the timer expires while the splash screen is running.
See Also
Ian Clifton’s blog post titled “Android Splash Screens Done Right” argues passionately for the dialog method.
2.7 Designing a Conference/Camp/Hackathon/Institution App
Ian Darwin
Problem
You want to design an app for use at a conference, BarCamp, or hackathon, or inside a large institution such as a hospital.
Solution
Provide at least the required functions listed in this recipe’s “Discussion” section, and as many of the optional ones as you think make sense.
Discussion
A good app of this type requires some or most of the following functions, as appropriate:
A map of the building, showing the locations of meetings, food services, washrooms, emergency exits, and so on. You get extra points if you provide a visual slider for moving up or down levels if your conference takes place on more than one floor or level in the building (think about a 3D fly-through of San Francisco’s Moscone Center, including the huge escalators). Remember that some people may know the building, but others will not. Consider having a “where am I” function (the user will type in the name or number of a room he sees; you get extra points if you offer visual matching or use the GPS instead of making the user type) as well as a “where is” function (the user selects from a list and the application jumps to the map view with a pushpin showing the desired location). Turn-by-turn walking directions through a maze of twisty little passages?
A map of the exhibit hall (if there is a show floor, have a map and an easy way to find a given exhibitor). Ditto for poster papers if your conference features these.
A schedule view. Highlight changes in red as they happen, including additions, last-minute cancellations, and room changes.
A sign-up button if your conference has Birds of a Feather (BOF) gatherings; you might even want a “Suggest a new BOF” Activity.
A local area map. This could be OpenStreetMap or Google Maps, or maybe something more detailed than the standard map. Add folklore, points of interest, navigation shortcuts, and other features. Limit it to a few blocks so that you can get the details right. A university campus is about the right size.
An overview map of the city. Again, this is not the Google map, but an artistic, neighborhood/zone view with just the highlights.
Tourist attractions within an hour of the site. Your mileage may vary.
A food finder. People always get tired of convention food and set out on foot to find something better to eat.
A friend finder. If Google’s Latitude app were open to use by third-party apps, you could tie into Google’s data. If it’s a security conference, implement this functionality yourself.
Private voice chat. If it’s a small security gathering, provide a Session Initiation Protocol (SIP) server on a well-connected host, with carefully controlled access; it should be possible to have almost walkie talkie–like service.
Sign-ups for impromptu group formation for trips to tourist attractions or any other purpose.
Functionality to post comments to Twitter, Facebook, and LinkedIn.
Note taking! Many people will have Android on large-screen tablets, so a “Notepad” equivalent, ideally linked to the session the notes are taken in, will be useful.
A way for users to signal chosen friends that they want to eat (at a certain time, in so many minutes, right now), including the type of food or restaurant name and seeing if they’re also interested.
See Also
The rest of this book shows how to implement most of these functions.
Google Maps has recently started serving building maps. The article shows who to contact to get your building’s internal locations added to the map data; if appropriate, consider getting the venue operators to give Google their building’s data.
2.8 Using Google Analytics in an Android Application
Ashwini Shahapurkar
Problem
Developers often want to track their applications in terms of features used by users. How can you determine which feature is most used by your app’s users?
Solution
Use Google Analytics to track the app based on defined criteria, similar to Google Analytics’s website-tracking mechanism.
Discussion
Before we use Google Analytics in our app, we need an analytics account which you can get for free from Google using one of two approaches to getting the Google Analytics SDK running:
Automated Approach
For Android Studio only, you can follow the steps to get the Analytics SDK given at https://developers.google.com/analytics/devguides/collection/android/resources, which involve having Google generate a simple configuration file containing your Analytics account, then adding two classpath dependencies and a Gradle plugin in your Gradle build scripts. The plugin will read your downloaded configuration file and apply the information to your code.
Now, sign in to your analytics account and create a website profile for the app. The website URL can be fake but should be descriptive. I recommend that you use the reverse package name for this. For example, if the application package name is com.example.analytics.test, the website URL for this app can be http://test.analytics.example.com. After you create the website profile, a web property ID is generated for that profile. Jot it down - save it in a safe place-as we will be using this ID in our app. The ID, also known as the UA number of the tracking code, uniquely identifies the website profile.
Common Steps
Next, ensure you have the following permissions in your project’s AndroidManifest.xml file:
For both legal and licensing reasons, you must inform your users that you are collecting anonymous user data in your app. You can do so via a policy statement, in the end-user license agreement, or somewhere else where users will see this information. See Recipe 2.9.
Now we are ready to track our application. Obtain the singleton instance of the tracker by calling the GoogleAnalytics.getInstance().newTracker() method. Usually, you will want to track more than Activities in the app. In such a scenario, it’s a good idea to have this tracker instance in the onCreate() method of the Application class of the app (see Example 2-9).
Example 2-9. The application implementation for tracking
publicclassGADemoAppextendsApplication{/* * Define web property ID obtained after creating a profile for the app. If * using the Gradle plugin, this should be available as R.xml.global_tracker. */privateStringwebId="UA-NNNNNNNN-Y";/* Analytics tracker instance */Trackertracker;/* This is the getter for the tracker instance. This is called from * within the Activity to get a reference to the tracker instance. */publicsynchronizedTrackergetTracker(){if(tracker==null){// Get the singleton Analytics instance, get Tracker from itGoogleAnalyticsinstance=GoogleAnalytics.getInstance(this);// Start tracking the app with your web property IDtracker=instance.newTracker(webId);// Any app-specific Application setup code goes here...}returntracker;}}
You can track page views and events in the Activity by calling the setScreenName() and send() methods on the tracker instance (see Example 2-10).
Example 2-10. The Main Activity with tracking
publicclassMainActivityextendsActivity{publicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);// Track the page view for the ActivityTrackertracker=((GADemoApp)getApplication()).getTracker();tracker.setScreenName("MainActivity");tracker.send(newHitBuilders.ScreenViewBuilder().build());/* You can track events like button clicks... */findViewById(R.id.actionButton).setOnClickListener(v->{Trackertracker=((GADemoApp)getApplication()).getTracker();tracker.send(newHitBuilders.EventBuilder("Action Event","Button Clicked").build());});}}
Using this mechanism, you can track all the Activities and events inside them. You then visit the Analytics web site to see how many times each Activity or other event has been invoked.
You have an application that collects app usage data anonymously, so you are obligated to make users aware of this the first time they run your application.
Solution
Use shared preferences as persistent storage to store a value, which gets updated only once. Each time the application launches, it will check for this value in the preferences. If the value has been set (is available), it is not the first run of the application; otherwise it is the first run.
Discussion
You can manage the application life cycle by using the Application class of the Android framework. We will use shared preferences as persistent storage to store the first-run value.
We will store a Boolean flag in the preferences if this is the first run. When the application is installed and used for the first time, there are no preferences available for it. They will be created for us. In that case the flag will return a value of true. After getting the true flag, we can update this flag with a value of false as we no longer need it to be true. See Example 2-11.
Example 2-11. First-run preferences
publicclassMyAppextendsApplication{SharedPreferencesmPrefs;@OverridepublicvoidonCreate(){super.onCreate();ContextmContext=this.getApplicationContext();// 0 = mode private. Only this app can read these preferences.mPrefs=mContext.getSharedPreferences("myAppPrefs",0);// Your app initialization code goes here}publicbooleangetFirstRun(){returnmPrefs.getBoolean("firstRun",true);}publicvoidsetRunned(){SharedPreferences.Editoredit=mPrefs.edit();edit.putBoolean("firstRun",false);edit.commit();}}
This flag from the preferences will be tested in the launcher Activity, as shown in Example 2-12.
Example 2-12. Checking whether this is the first run of this app
if(((MyApp)getApplication()).getFirstRun()){// This is the first run((MyApp)getApplication()).setRunned();// Your code for the first run goes here}else{// This is not the first run on this device}
Even if you publish updates for the app and the user installs the updates, these preferences will not be modified; therefore, the code will work for only the first run after installation. Subsequent updates to the app will not bring the code into the picture, unless the user has manually uninstalled and reinstalled the app.
Note
You could use a similar technique for distributing shareware versions of an Android app (i.e., limit the number of trials of the application). In this case, you would use an integer count value in the preferences to indicate the number of trials. Each trial would update the preferences. After the desired value is reached, you would block the usage of the application until the user pays the usage fee.
2.10 Formatting Numbers
Ian Darwin
Problem
You need to format numbers, because the default formatting of Double.toString() and friends does not give you enough control over how the results are displayed.
Solution
Use String.format() or one of the NumberFormat subclasses.
Discussion
The printf() function was first included in the C programming language in the 1970s, and it has been used in many other languages since, including Java. Here’s a simple printf() example in Java SE:
System.out.printf("Hello %s at %s%n",userName,time);
The preceding example could be expected to print something like this:
HelloRobinatWedJun1608:38:46EDT2010
Since we don’t use System.out in Android, you’ll be relieved to note that you can get the same string that would be printed, for putting it into a view, by using:
Stringmsg=String.format("Hello %s at %s%n",userName,time);
If you haven’t seen printf() before, the first argument is obviously the format code string, and any other arguments here, (userName and time) are values to be formatted. The format codes begin with a percent sign (%) and have at least one “type” code; Table 2-1 shows some common type codes.
Table 2-1. Some common format codes
Character
Meaning
s
String (convert primitive values using defaults; convert objects by toString)
d
Decimal integer (int, long)
f
Floating point (float, double)
n
Newline
t
Time/date formats, Java-specific; see the discussion referred to in the “See Also” section at the end of the recipe
The default date formatting is pretty ugly, so we often need to expand on it. The printf() formatting capabilities are actually housed in the java.util.Formatter class, to which reference should be made for the full details of its formatting language.
Unlike printf() in other languages you may have used, all these format routines optionally allow you to refer to arguments by their number, by putting a number plus a dollar sign after the % lead-in but before the formatting code proper; for example, %2$3.1f means to format the second argument as a decimal number with three characters and one digit after the decimal place. This numbering can be used for two purposes: to change the order in which arguments print (often useful with internationalization), and to refer to a given argument more than once. The date/time format character t requires a second character after it, such as Y for the year, m for the month, and so on. Here we take the time argument and extract several fields from it:
msg = String.format("Hello at %1$tB %1$td, %1$tY%n", time);
This might format as July 4, 2010.
To print numbers with a specific precision, you can use f with a width and a precision, such as:
While such formatting is OK for specific uses such as latitudes and longitudes, for general use such as currencies, it may give you too much control.
General formatters
Java has an entire package, java.text, that is full of formatting routines as general and flexible as anything you might imagine. Like printf(), it has an involved formatting language, described in the online documentation page. Consider the presentation of numbers. In North America, the number “one thousand twenty-four and a quarter” is written 1,024.25; in most of Europe it is 1 024,25, and in some other parts of the world it might be written 1.024,25. The formatting of currencies and percentages is equally varied. Trying to keep track of this yourself would drive the average software developer around the bend rather quickly.
Fortunately, the java.text package includes a Locale class. Furthermore, the Java or Android runtime automatically sets a default Locale object based on the user’s environment; this code works the same on desktop Java as it does in Android. To provide formatters customized for numbers, currencies, and percentages, the NumberFormat class has static factory methods that normally return a DecimalFormat with the correct pattern already instantiated. A DecimalFormat object appropriate to the user’s locale can be obtained from the factory method NumberFormat.getInstance() and manipulated using set methods. Surprisingly, the method setMinimumIntegerDigits() turns out to be the easy way to generate a number format with leading zeros. Example 2-13 is an example.
Example 2-13. Number formatting demo
importjava.text.NumberFormat;/* * Format a number our way and the default way. */publicclassNumFormat2{/** A number to format */publicstaticfinaldoubledata[]={0,1,22d/7,100.2345678};publicstaticvoidmain(String[]av){// Get a format instanceNumberFormatform=NumberFormat.getInstance();// Tailor it to look like 999.99[99]form.setMinimumIntegerDigits(3);form.setMinimumFractionDigits(2);form.setMaximumFractionDigits(4);// Now print using itfor(inti=0;i<data.length;i++)System.out.println(data[i]+" formats as "+form.format(data[i]));}}
This prints the contents of the array using the NumberFormat instance form. We show running it as a main program instead of in an Android application just to isolate the effects of the NumberFormat.
For example, $ java NumFormat2 0.0 formats as 000.00; with the argument 1.0 it formats as 001.00, with 3.142857142857143 it formats as 003.1429, and with 100.2345678 it formats as 100.2346.
You can also construct a DecimalFormat with a particular pattern or change the pattern dynamically using applyPattern(). Table 2-2 shows some of the more common pattern characters.
Table 2-2. Common DecimalFormat pattern characters
Character
Explanation
#
Numeric digit (leading zeros suppressed)
0
Numeric digit (leading zeros provided)
.
Locale-specific decimal separator (decimal point)
,
Locale-specific grouping separator (comma in English)
-
Locale-specific negative indicator (minus sign)
%
Shows the value as a percentage
;
Separates two formats: the first for positive and the second for negative values
'
Escapes one of the preceding characters so that it appears as itself
Anything else
Appears as itself
The NumFormatTest program uses one DecimalFormat to print a number with only two decimal places and a second to format the number according to the default locale, as shown in Example 2-14.
Example 2-14. NumberFormat demo Java SE program
importjava.text.DecimalFormat;importjava.text.NumberFormat;publicclassNumFormatDemo{/** A number to format */publicstaticfinaldoubleintlNumber=1024.25;/** Another number to format */publicstaticfinaldoubleourNumber=100.2345678;publicstaticvoidmain(String[]av){NumberFormatdefForm=NumberFormat.getInstance();NumberFormatourForm=newDecimalFormat("##0.##");// toPattern() will reveal the combination of #0., etc.// that this particular Locale uses to format withSystem.out.println("defForm's pattern is "+((DecimalFormat)defForm).toPattern());System.out.println(intlNumber+" formats as "+defForm.format(intlNumber));System.out.println(ourNumber+" formats as "+ourForm.format(ourNumber));System.out.println(ourNumber+" formats as "+defForm.format(ourNumber)+" using the default format");}}
This program prints the given pattern and then formats the same number using several formats:
$ java NumFormatTest
defForm's pattern is #,##0.###
1024.25 formats as 1,024.25
100.2345678 formats as 100.23
100.2345678 formats as 100.235 using the default format
See Also
Chapter 10 of my book Java Cookbook and Part VI of [Java I/O] by Elliotte Rusty Harold (both from O’Reilly).
2.11 Formatting with Correct Plurals
Ian Darwin
Problem
You’re displaying something like "Found "+ n +" reviews", but in English, “Found 1 reviews” is ungrammatical. You want "Found 1 review" for the case n==1.
Solution
For simple, English-only results, use a conditional statement. For better results that can be internationalized, use a ChoiceFormat. On Android, you can use <plural> in an XML resources file.
Discussion
The “quick and dirty” method is to use Java’s ternary operator (cond ? trueval : falseval) in a string concatenation. Since in English, for most nouns, both zero and plurals get an s appended to the noun (“no books, one book, two books”), we need only test for n==1:
// FormatPlurals.javapublicstaticvoidmain(Stringargv[]){report(0);report(1);report(2);}/** report -- using conditional operator */publicstaticvoidreport(intn){System.out.println("Found "+n+" item"+(n==1?"":"s"));}
Running this on Java SE as a main program shows the following output:
$ java FormatPlurals
Found 0 items
Found 1 item
Found 2 items
This is longer, so Java’s ternary conditional operator is worth learning.
Of course, you can’t use this arbitrarily, because English is a strange and somewhat idiosyncratic language. Some nouns, such as bus, require “es” at the end, while others, such as cash, are collective nouns with no plural (you can have two flocks of geese or two stacks of cash, but you cannot have “two geeses” or “two cashes”). Still other nouns, such as fish, can be considered plural as they are, although fishes is also a correct plural.
A better way
The ChoiceFormat class from java.text is ideal for handling plurals; it lets you specify singular and plural (or, more generally, range) variations on the noun. It is capable of more, but in Example 2-15 I’ll show only a couple of the simpler uses. I specify the values 0, 1, and 2 (or more), and the string values to print corresponding to each number. The numbers are then formatted according to the range they fall into.
Example 2-15. Formatting plurals using ChoiceFormat
importjava.text.*;/** * Format a plural correctly, using a ChoiceFormat. */publicclassFormatPluralsChoiceextendsFormatPlurals{// ChoiceFormat to just give pluralized wordstaticdouble[]limits={0,1,2};staticString[]formats={"reviews","review","reviews"};staticChoiceFormatpluralizedFormat=newChoiceFormat(limits,formats);// ChoiceFormat to give English text version, quantifiedstaticChoiceFormatquantizedFormat=newChoiceFormat("0#no reviews|1#one review|1<many reviews");// Test datastaticint[]data={-1,0,1,2,3};publicstaticvoidmain(String[]argv){System.out.println("Pluralized Format");for(inti:data){System.out.println("Found "+i+" "+pluralizedFormat.format(i));}System.out.println("Quantized Format");for(inti:data){System.out.println("Found "+quantizedFormat.format(i));}}}
Either of these loops generates output similar to the basic version. The code using the ChoiceFormat is slightly longer, but more general, and lends itself better to internationalization. Put the string for the “quantized” form constructor into strings.xml and it will be part of your localization actions.
The best way (Android only)
Create a file in /res/values$$/ containing something like this:
Example 2-17 obtains the current time and date using the java.util.Date class and then displays it in different formats (please refer to the comments for sample output).
Example 2-17. The date formatter Activity
packagecom.sym.dateformatdemo;importjava.util.Date;importandroid.app.Activity;importandroid.os.Bundle;importandroid.text.format.DateFormat;importandroid.widget.TextView;publicclassTestDateFormatterActivityextendsActivity{/** Called when the Activity is first created. */@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);TextViewtextView1=(TextView)findViewById(R.id.textview1);TextViewtextView2=(TextView)findViewById(R.id.textview2);TextViewtextView3=(TextView)findViewById(R.id.textview3);TextViewtextView4=(TextView)findViewById(R.id.textview4);TextViewtextView5=(TextView)findViewById(R.id.textview5);Stringdelegate="MM/dd/yy hh:mm a";// 09/21/2011 02:17 pmDatenoteTS=newDate();textView1.setText("Found Time :: "+DateFormat.format(delegate,noteTS));delegate="MMM dd, yyyy h:mm aa";// Sep 21,2011 02:17 pmtextView2.setText("Found Time :: "+DateFormat.format(delegate,noteTS));delegate="MMMM dd, yyyy h:mmaa";// September 21,2011 02:17pmtextView3.setText("Found Time :: "+DateFormat.format(delegate,noteTS));delegate="E, MMMM dd, yyyy h:mm:ss aa";//Wed, September 21,2011 02:17:48 pmtextView4.setText("Found Time :: "+DateFormat.format(delegate,noteTS));delegate="EEEE, MMMM dd, yyyy h:mm aa";//Wednesday, September 21,2011 02:17:48 pmtextView5.setText("Found Time :: "+DateFormat.format(delegate,noteTS));}}
See Also
Recipe 2.13. Also, the classes shown in the following table, in the package android.text.format, may be of use in this type of application.
Name
Usage
DateUtils
This class contains various date-related utilities for creating text for things like elapsed time and date ranges, strings for days of the week and months, and a.m./p.m. text.
Formatter
This is a utility class to aid in formatting common values that are not covered by java.util.Formatter.
Time
This class is a faster replacement for the java.util.Calendar and java.util.GregorianCalendar classes.
2.13 Simplifying Date/Time Calculations with the Java 8 java.time API
Ian Darwin
Problem
You’ve heard that the JSR-310 date/time API, included in Java SE 8, simplifies date and time calculations, and you’d like to use it in Android.
Solution
You can use the new java.time API in Android O and later.
Since Android did not become fully compliant with JDK 8 even in Android Nougat, despite being “based on” OpenJDK 8, for Android Nougat and earlier, you must use a third-party library such as the JSR-310 “backport” to access the java.time facilities, albeit with a different package name.
Discussion
There is a long history to the java.time API that I won’t bore you with here; suffice it to say that we are all indebted to Steven Colbourne for inventing it and for his constancy in urging first Sun, then Oracle, to incorporate it into Java, which finally happened in Java 8. For licensing reasons, the backport of JSR-310—by its original author—to Java 6/7 was placed in a non-Java package, org.threeten.bp.
Since Android N didn’t provide full compatibility with Java 8, we use an external library. We’ll use an Android-specific version of this “backport” library, by Jake Wharton, is available on GitHub.
You can add it to any Gradle or Maven project just by adding the coordinates compile 'com.jakewharton.threetenabp:threetenabp:1.0.3' to your build script (the version number may change over time, of course).
Here is an example to show you the level of complexity of the kinds of calculations that are built in.
I’ve omitted the imports because they differ from the backport libraries and “standard Java” and Android O.
The example shows how little code is needed to figure out the day of the month on which the next weekly and monthly paydays occur:
LocalDateTimenow=LocalDateTime.now();LocalDateTimeweeklyPayDay=now.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));weekly.setText("Weekly employees' payday is Friday "+weeklyPayDay.getMonth()+" "+weeklyPayDay.getDayOfMonth());LocalDateTimemonthlyPayDay=now.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));monthly.setText("Monthly employees are paid on "+monthlyPayDay.getMonth()+" "+monthlyPayDay.getDayOfMonth());
The API includes LocalDate objects, which just represent one particular day; LocalTime objects, which represent a time of day; and LocalDateTime objects, which represent a date and a time. As the names imply, all three are local, not meant to represent time across the world’s time zones. For that, you want to use one of several classes that represent time zones. See the java.time documentation for details of all the classes.
To use the backport library on Android N and earlier, you need one extra call to initialize it, either in your Application class (see Recipe 2.3) or in your Activity. In the main Activity’s onCreate() method you’d say:
The new API is covered in Chapter 6 of my Java Cookbook and in some tutorials on the web. Make sure you use the version of the tutorial corresponding to the API you are using. The Java 8 version differs slightly from “ThreeTen” versions, and these both differ from the original Joda Time versions.
Your application contains text boxes in which you want to restrict users to entering only numbers; also, in some cases you want to allow only positive numbers, or integers, or dates.
Solution
Android provides KeyListener classes to help you restrict users to entering only numbers, positive numbers, integers, positive integers, and much more.
Discussion
The Android.text.method package includes a KeyListener interface, along with some classes such as DigitsKeyListener and DateKeyListener that implement this interface.
Example 2-18 is a sample application that demonstrates a few of these classes. This layout file creates five TextViews and five EditTexts; the TextViews display the input type allowed for their respective EditTexts.
Example 2-18. Layout with TextViews and EditTexts
<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/textview1"android:text="digits listener with signs and decimal points"/><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/editText1"/><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/textview2"android:text="digits listener without signs and decimal points"/><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/editText2"/><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/textview3"android:text="date listener"/><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/editText3"/><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/textview4"android:text="multitap listener"/><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/editText4"/><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/textview5"android:text="qwerty listener"/><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:id="@+id/editText5"/></LinearLayout>
Example 2-19 is the code for the Activity that restricts the EditText inputs to numbers, positive integers, and so on (refer to the comments for groups of keys allowed).
Example 2-19. The main Activity
importandroid.app.Activity;importandroid.os.Bundle;importandroid.text.method.DateKeyListener;importandroid.text.method.DigitsKeyListener;importandroid.text.method.MultiTapKeyListener;importandroid.text.method.QwertyKeyListener;importandroid.text.method.TextKeyListener;importandroid.widget.EditText;publicclassKeyListenerDemoextendsActivity{/** Called when the Activity is first created. */@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);// Allows digits with positive/negative signs and decimal pointsEditTexteditText1=(EditText)findViewById(R.id.editText1);DigitsKeyListenerdigkl1=DigitsKeyListener.getInstance(true,true);editText1.setKeyListener(digkl1);// Allows positive integers only (no decimal values allowed)EditTexteditText2=(EditText)findViewById(R.id.editText2);DigitsKeyListenerdigkl2=DigitsKeyListener.getInstance();editText2.setKeyListener(digkl2);// Allows dates onlyEditTexteditText3=(EditText)findViewById(R.id.editText3);DateKeyListenerdtkl=newDateKeyListener();editText3.setKeyListener(dtkl);// Allows multitap with 12-key keypad layoutEditTexteditText4=(EditText)findViewById(R.id.editText4);MultiTapKeyListenermultitapkl=newMultiTapKeyListener(TextKeyListener.Capitalize.WORDS,true);editText4.setKeyListener(multitapkl);// Allows qwerty layout for typingEditTexteditText5=(EditText)findViewById(R.id.editText5);QwertyKeyListenerqkl=newQwertyKeyListener(TextKeyListener.Capitalize.SENTENCES,true);editText5.setKeyListener(qkl);}}
To use MultiTapKeyListener, your phone should support the 12-key layout and it needs to be activated. To activate the 12-key layout, go to Settings → Language and Keyboard → On-screen Keyboard Layout and then select the “Phone layout” options.
See Also
The Listener types in the following table will be of use in writing this type of application.
Name
Usage
BaseKeyListener
This is an abstract base class for key listeners.
DateTimeKeyListener
This is for entering dates and times in the same text field.
MetaKeyKeyListener
This base class encapsulates the behavior for tracking the state of meta keys such as SHIFT, ALT, and SYM, as well as the pseudo-meta state of selecting text.
NumberKeyListener
This is for numeric text entry.
TextKeyListener
This is the key listener for typing normal text.
TimeKeyListener
This is for entering times in a text field.
2.15 Backing Up Android Application Data
Pratik Rupwal
Problem
When a user performs a factory reset or converts to a new Android-powered device, the application loses stored data or application settings.
Solution
Android’s Backup Manager helps to automatically restore backup data or application settings when the application is reinstalled.
Discussion
Android’s Backup Manager basically operates in two modes: backup and restore. During a backup operation, the Backup Manager (BackupManager class) queries your application for backup data, then hands it to a backup transport, which then delivers the data to cloud-based storage. During a restore operation, the Backup Manager retrieves the backup data from the backup transport and returns it to your application so that your application can restore the data to the device. It’s possible for your application to request a restore, but not necessary because Android performs a restore operation when your application is installed and backup data associated with the user exists. The primary scenario in which backup data is restored happens when a user resets her device or upgrades to a new device and her previously installed applications are reinstalled.
Example 2-20 shows how to implement the Backup Manager for your application so that you can save the current state of your application.
Here is a basic description of the procedure in step-by-step form:
Create a BackupManagerExample project in Eclipse.
Open the layout/backup_restore.xml file and insert the code in Example 2-20 into it.
Open the values/string.xml file and insert into it the code shown in Example 2-21.
Your manifest file will look like the code shown in Example 2-22.
The code in Example 2-23 completes the implementation of the Backup Manager for your application.
Example 2-21. Strings for the example
<resources><stringname="hello">Hello World, BackupManager!</string><stringname="app_name">BackupManager</string><stringname="filling_text">Choose Settings for your application:</string><stringname="bacon_label">Sound On</string><stringname="pastrami_label">Vibration On</string><stringname="hummus_label">Backlight On</string><stringname="extras_text">Extras:</string><stringname="mayo_text">Use Orientation?</string><stringname="tomato_text">Use Camera?</string></resources>
Example 2-22. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.sym.backupmanager"android:versionCode="1"android:versionName="1.0"><uses-sdkandroid:minSdkVersion="9"/><applicationandroid:label="Backup/Restore"android:icon="@drawable/icon"android:backupAgent="ExampleAgent"><!--Here you specify the backup agent--><!--Some backup transports may require API keys or other metadata--><meta-dataandroid:name="com.google.android.backup.api_key"android:value="INSERT YOUR API KEY HERE"/><activityandroid:name=".BackupManagerExample"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application></manifest>
Example 2-23. The backup/restore Activity
packagecom.sym.backupmanager;importandroid.app.Activity;importandroid.app.backup.BackupManager;importandroid.os.Bundle;importandroid.util.Log;importandroid.widget.CheckBox;importandroid.widget.CompoundButton;importandroid.widget.RadioGroup;importjava.io.File;importjava.io.IOException;importjava.io.RandomAccessFile;publicclassBackupManagerExampleextendsActivity{staticfinalStringTAG="BRActivity";staticfinalObject[]sDataLock=newObject[0];staticfinalStringDATA_FILE_NAME="saved_data";RadioGroupmFillingGroup;CheckBoxmAddMayoCheckbox;CheckBoxmAddTomatoCheckbox;FilemDataFile;BackupManagermBackupManager;@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.backup_restore);mFillingGroup=(RadioGroup)findViewById(R.id.filling_group);mAddMayoCheckbox=(CheckBox)findViewById(R.id.mayo);mAddTomatoCheckbox=(CheckBox)findViewById(R.id.tomato);mDataFile=newFile(getFilesDir(),BackupManagerExample.DATA_FILE_NAME);mBackupManager=newBackupManager(this);populateUI();}voidpopulateUI(){RandomAccessFilefile;intwhichFilling=R.id.pastrami;booleanaddMayo=false;booleanaddTomato=false;synchronized(BackupManagerExample.sDataLock){booleanexists=mDataFile.exists();try{file=newRandomAccessFile(mDataFile,"rw");if(exists){Log.v(TAG,"datafile exists");whichFilling=file.readInt();addMayo=file.readBoolean();addTomato=file.readBoolean();Log.v(TAG," mayo="+addMayo+" tomato="+addTomato+" filling="+whichFilling);}else{Log.v(TAG,"creating default datafile");writeDataToFileLocked(file,addMayo,addTomato,whichFilling);mBackupManager.dataChanged();}}catch(IOExceptionioe){// Do some error handling here!}}mFillingGroup.check(whichFilling);mAddMayoCheckbox.setChecked(addMayo);mAddTomatoCheckbox.setChecked(addTomato);mFillingGroup.setOnCheckedChangeListener(newRadioGroup.OnCheckedChangeListener(){publicvoidonCheckedChanged(RadioGroupgroup,intcheckedId){Log.v(TAG,"New radio item selected: "+checkedId);recordNewUIState();}});CompoundButton.OnCheckedChangeListenercheckListener=newCompoundButton.OnCheckedChangeListener(){publicvoidonCheckedChanged(CompoundButtonbuttonView,booleanisChecked){Log.v(TAG,"Checkbox toggled: "+buttonView);recordNewUIState();}};mAddMayoCheckbox.setOnCheckedChangeListener(checkListener);mAddTomatoCheckbox.setOnCheckedChangeListener(checkListener);}voidwriteDataToFileLocked(RandomAccessFilefile,booleanaddMayo,booleanaddTomato,intwhichFilling)throwsIOException{file.setLength(0L);file.writeInt(whichFilling);file.writeBoolean(addMayo);file.writeBoolean(addTomato);Log.v(TAG,"NEW STATE: mayo="+addMayo+" tomato="+addTomato+" filling="+whichFilling);}voidrecordNewUIState(){booleanaddMayo=mAddMayoCheckbox.isChecked();booleanaddTomato=mAddTomatoCheckbox.isChecked();intwhichFilling=mFillingGroup.getCheckedRadioButtonId();try{synchronized(BackupManagerExample.sDataLock){RandomAccessFilefile=newRandomAccessFile(mDataFile,"rw");writeDataToFileLocked(file,addMayo,addTomato,whichFilling);}}catch(IOExceptione){Log.e(TAG,"Unable to record new UI state");}mBackupManager.dataChanged();}}
Data backup is not guaranteed to be available on all Android-powered devices. However, your application is not adversely affected in the event that a device does not provide a backup transport. If you believe that users will benefit from data backup in your application, you can implement it as described in this recipe, test it, and then publish your application without any concern about which devices actually perform backups. When your application runs on a device that does not provide a backup transport, the application will operate normally but will not receive callbacks from the Backup Manager to back up data.
Although you cannot know what the current transport is, you are always assured that your backup data cannot be read by other applications on the device. Only the Backup Manager and backup transport have access to the data you provide during a backup operation.
Warning
Because the cloud storage and transport services can differ among devices, Android makes no guarantees about the security of your data while using backup. You should always be cautious about using backup to store sensitive data, such as usernames and passwords.
Testing your backup agent
Once you’ve implemented your backup agent, you can use the bmgr command to test the backup and restore functionality by following these steps:
Install your application on a suitable Android system image, running any current emulator or device with Google Play Services.
Ensure that backup capability is enabled. If you are using the emulator, you can enable backup with the following command from your SDK tools/path:
$ adb shell bmgr enable true
If you are using a device, open the system settings, select Privacy, and then enable “Back up my data” and “Automatic restore.”
Open your application and initialize some data.
If you’ve properly implemented backup capability in your application, it should request a backup each time the data changes. For example, each time the user changes some data, your app should call dataChanged(), which adds a backup request to the Backup Manager queue. For testing purposes, you can also make a request with the following ++bmgr++ command:
$ adb shell bmgr backup your.package.name
Initiate a backup operation:
$ adb shell bmgr run
This forces the Backup Manager to perform all backup requests that are in its queue.
Uninstall your application:
$ adb uninstall your.package.name
Reinstall your application.
If your backup agent is successful, all the data you initialized in step 4 is restored.
2.16 Using Hints Instead of Tool Tips
Daniel Fowler
Problem
Android devices can have small screens, so there may not be room for help text, and tool tips are not part of the platform.
Solution
Android provides the hint attribute for Views.
Discussion
Sometimes an input field needs clarification with regard to the value that should be entered. For example, a stock-ordering application asking for item quantities may need to state the minimum order size. In desktop programs, with large screens and the use of a mouse, extra messages can be displayed in the form of tool tips (a pop-up label over a field when the mouse moves over it). Alternatively, long descriptive labels may be used. With Android devices, the screen may be small and no mouse is generally used. The alternative here is to use the android:hint attribute on a View. This causes a “watermark” containing the hint text to be displayed in the input field when it is empty; this disappears when the user starts typing in the field. The corresponding function for android:hint is setHint(int resourceId). Figure 2-5 shows an example hint.
You can set the color of the hint text with android:textColorHint, with setHintTextColor(int color) being the associated function.
Using hints can also help with screen layouts when space is tight. A screen design can sometimes be improved by removing a label and using a hint, as shown in Figure 2-6.
The EditText definition in Figure 2-6 is shown in the following code so that you can see android:hint in use:
<EditTextandroid:id="@+id/etQuantity"android:layout_width="fill_parent"android:layout_height="wrap_content"android:hint="Number of boxes of ten"android:textSize="18sp"/>
Hints can guide users as they are filling in app fields, though as with any feature overuse is possible. Hints should not be used when it is obvious what is required; a field with a label of “First Name” would not need a hint such as “Enter your first name here,” for example. Figure 2-6 shows our hypothetical ordering application improved somewhat by removing the redundant label.