Chapter    19

Exploring Maps and Location-Based Services

In this chapter, we are going to talk about maps and location-based services. Location-based services form one of the more exciting pieces of the Android SDK. This portion of the SDK provides APIs to let application developers display and manipulate maps, obtain real-time device-location information, and take advantage of other exciting features. Working with maps changed significantly when Google introduced the MapFragment and version 2 of the Google Maps API. This chapter will go into details of the new ways of creating maps and manipulating them.

The location-based services facility in Android sits on two pillars: the mapping and location-based APIs. The mapping APIs in Android provide facilities for you to display a map and manipulate it. For example, you can zoom and pan; you can change the map mode (from satellite view to traffic view, for example); you can add markers and custom data to the map; and so on. The other end of the spectrum is Global Positioning System (GPS) data and information about locations, both of which are handled by the location package.

These APIs often reach across the Internet to invoke services from Google servers, via Google Play Services (the local uber application on the device). Therefore, you will usually need to have Internet connectivity for these to work. In addition, Google has Terms of Service that you must agree to before you can develop applications with these Google services. Read the terms carefully; Google places some restrictions on what you can do with the service data. For example, you can use location information for users’ personal use, but certain commercial uses are restricted, as are applications involving automated control of vehicles. The terms will be presented to you when you sign up for a Maps API key.

In this chapter, we’ll go through each of these packages. We’ll start with the mapping APIs and show you how to use maps with your applications. As you’ll see, mapping in Android boils down to using MapFragment class in addition to the mapping APIs, which integrate with Google Maps. We will also show you how to place custom data onto the maps that you display and how to show the current location of the device on a map. After talking about maps, we’ll delve into location-based services, which extend the mapping concepts. We will show you how to use the Android Geocoder class and the LocationServices service. We will also touch on threading issues that surface when you use these APIs.

Understanding the Mapping Package

As we mentioned, the mapping APIs are one of the components of Android’s location-based services. The mapping package contains almost everything you’ll need to display a map on the screen, handle user interaction with the map (such as zooming), display custom data on top of the map, and so on. In the old version of Android Maps, your application would talk directly to the Google Maps services for everything map-related. In the new version, your application must talk to Google Play Services, which is a local app on the device, provided as part of the operating system. Your app still also makes calls over the Internet for data, but if Google Play Services is not present locally on the device, your maps will not work. If you need maps on devices that don’t have Google Play Services you’ll need to explore one of the other maps packages available for Android (e.g., MapQuest).

In order for your application to talk to Google Play Services, you will need to include the Google Play Services library into your application. Android Studio does this differently than Eclipse with ADT. See the References section below for a link to online instructions for the latest way to do this. Before you include the Google Play Services library in your application, you must first download it through the SDK Manager. You’ll find it under Extras.

You may have noticed that your Android SDK Manager shows Google API packages in addition to the Android SDK platforms. Previously, you had to base your application on a Google APIs package in order to use maps, but that is no longer true. Instead, the Maps API integrates to Google Play Services, so your application can be based on a regular Android package. However, to test a maps-based app in the emulator, you would need to base your emulator’s Android Virtual Device (AVD) on a Google APIs package. More on testing apps later.

The first step to working with the maps package is to display a map. To do that, you’ll use MapFragment (or SupportMapFragment if you want backwards compatibility with versions of Android prior to API 12, a.k.a. Honeycomb 3.1). Using this class, however, requires some preparation work. Specifically, before you can use Google Maps services, you’ll need to get a Maps API key from Google. The Maps API key enables Android to interact with Google Maps services to obtain map data. The next section explains how to obtain a Maps API key.

Obtaining a Maps API Key from Google

Google wants to be able to identify the application that is connecting to the map services. It uses a combination of the application package and the certificate used to sign the application, to generate a Maps API key that the application must then use to request service. The Maps API key can be used across a number of pairs of packages and certificates. This means you can use the same Maps API key for development and production; the package would be the same but the certificates are probably different. In theory you could use the same key across multiple applications but this practice is discouraged. You don’t want to do this anyway since Google imposes certain limits on the Maps API usage and by sharing a Maps API key with multiple applications you could more easily exceed the limit.

To obtain a Maps API key, you need the certificate that you’ll use to sign your application (in the case of a development version of your app, the debug certificate). You’ll get the SHA-1 fingerprint of your certificate, and then you’ll enter it, along with your application’s package, on Google’s web site to generate an associated Maps API key.

First, you must locate your debug certificate, which is generated and maintained by Eclipse. You can find the exact location using the Eclipse IDE. If you’re using an IDE other than Eclipse, you just need to locate the keystore file where the certificates are held. From Eclipse’s Preferences menu, go to Android image Build. The debug certificate’s location will be displayed in the Default Debug Keystore field, as shown in Figure 19-1.

9781430246800_Fig19-01.jpg

Figure 19-1. The debug certificate’s location

To extract the SHA-1 fingerprint, you can run the keytool with the –list option, as shown here:

keytool -list -alias androiddebugkey -keystore
"FULL PATH OF YOUR debug.keystore FILE" -storepass android -keypass android

Note that the alias you want from the debug store is androiddebugkey. Similarly, the keystore password is android, and the private key password is also android. When you run this command, the keytool provides the fingerprint (see Figure 19-2).

9781430246800_Fig19-02.jpg

Figure 19-2. The keytool output for the list option

You’ll notice that the fingerprint displayed by the keytool command is the same as displayed in the Preferences screen as shown in Figure 19-1 so you could have just gotten it from that screen. But now you know both ways to extract out the SHA-1 fingerprint for your application. When you use keytool to extract out the SHA-1 fingerprint for the production certificate, you’ll use the keystore file, alias, and password that you set up for your production certificate.

The next step is to go to Google’s Developer Console to add your application, and following that you enable the Maps API. The result will be your Maps API key to include in your application. The Developer Console is here, and you will need a Google account in order to get there:

https://console.developers.google.com

You will need to create a new project. As part of creating the new project, you need to provide a Project Name and a Project ID. The Project ID will be pre-populated with something strange-looking. You can put any value you want here as long as it is unique. However, the Project ID is just for use by the Google Developer Console; it has nothing to do with the source code of your application. Remember that you’re creating a sample project based on the code of this chapter’s sample project, so that you can get a Maps API key to see it work.

Read through the Terms of Service. If you agree to the terms, click Create to create your new project. This sets up a basic template of a project with Google. Next you’re going to enable the APIs you want. For a maps application, you’ll choose Google Maps Android API v2. For the chapter’s sample application, you also want to include the Geocoding API. You might get a pop-up window called ‘Configure Android Key for <your app name>’. If you don’t get a pop-up, you can navigate to the APIs & auth image Credentials section of your project in the Developers Console and generate an API key there. This is where you need to copy and paste in both the SHA-1 fingerprint of the application signing certificate, and the package name of the application, separated by a semicolon. The package name is the one from your source code. Note that you can copy in more than one line, so if you have the SHA-1 fingerprint from the production application signing certificate (which is typically different from the androiddebugkey used in development), you could add a second line for the production application.

Once you press the Create button on this screen you will get an API key. This is what you will include in the AndroidManifest.xml file of your application. The API key is active immediately, so you can start using it to obtain map data from Google.

Adding the Maps API Key to Your Application

To see how the Maps API key is added to the manifest file, see the bottom of Listing 19-1.

Listing 19-1. AndroidManifest.xml for a Simple Maps Application

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.androidbook.maps.whereami"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="10" android:targetSdkVersion="19" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-feature
        android:glEsVersion="0x00020000" android:required="true"/>

    <application
        android:allowBackup="true" android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.androidbook.maps.whereami.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <meta-data android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="AIzaSyBDs1ZQgu9X2A4TG1a7fPl-Ge_MKlyviKM"/>
    </application>
</manifest>

As you no doubt noticed, there are other elements within the manifest file that must be present for a maps application to work. The <meta-data> tag above the Maps API key is required, as are the permissions near the top. Technically, the ACCESS_FINE_LOCATION permission is not needed to show maps; it is there so location functionality (e.g., GPS) will work. GPS is commonly used in maps applications. ACCESS_NETWORK_STATE and INTERNET permissions are there so the maps application can download map tile data (i.e., the map graphics) and to know what type and state of network connection the application has. The WRITE_EXTERNAL_STORAGE permission is there so the maps application can create a local cache of map tile files on the device’s local storage space. Without caching, a maps application would likely spend a lot of time downloading map tiles over and over again, which is not only inefficient for your app, but it places an unwanted burden on the Google servers and it could consume a large portion of the user’s data plan. And finally, the glEsVersion feature is present because rendering maps on the screen uses OpenGL, so by requiring the feature, the application avoids getting installed on devices that could not display maps.

Now, let’s start playing with maps.

Understanding MapFragment

The foundational building block of a map application is the MapFragment. This was introduced in Honeycomb (Android 3.1) and replaced MapView and MapActivity functionality. Now you can embed a MapFragment inside of a regular Android activity. If you want your application to run on devices running an older version of Android, you can use SupportMapFragment and embed it inside a FragmentActivity. The MapFragment contains the map view to display maps, it handles user gestures to manipulate the map, and it manages the background threads that talk to the Google services to retrieve map data.

MapFragment is a very nice bundle of functionality, but it’s not all that you need on your device to make maps work. Fortunately, the integration with Google Play Services is all handled for you; all you need to do is make a special entry into your app’s AndroidManifest.xml file, which you saw in the previous section.

The first sample application for this chapter will simply show a map to the user and let the user explore the map.

Note  NoteWe give you a URL at the end of the chapter that you can use to download projects from this chapter. This will allow you to import these projects into your IDE directly. Also note that if you want to test these samples with an Android emulator, make sure the Android Virtual Device (AVD) is built with the Google APIs.

Please refer to the sample project called WhereAmI. The application is made up of a very basic FragmentActivity, a very simple layout, and a SupportMapFragment. The sample is using the compatibility classes, which means it will run on Gingerbread devices as well as the latest models. If your app only needs to run on devices newer than Honeycomb 3.0, you could use a regular activity and a MapFragment instead.

Listing 19-2 shows the activity. All that’s needed is to set up the layout and, if needed, create the MapFragment and insert it into the layout’s container (a FrameLayout).

If the activity is re-created due to an orientation change for example, the map fragment will still be available and be automatically attached to the new activity by Android. If the map fragment is not found, it means this is the first time in, or the map fragment has been destroyed, so create a new one and attach it. It doesn’t get any easier than that. The layout source is shown in Listing 19-3. It is simply a FrameLayout with an id of "container" that fills the available screen space.

Listing 19-3. Layout for Simple Map Display (activity_main.xml)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.androidbook.maps.whereami.MainActivity"
    tools:ignore="MergeRootFrame" />

If you are including the map fragment with other items in your user interface, you can simply use the FrameLayout where you want the map fragment to appear, embedded within other layouts. The only code remaining is that of the MapFragment, which is shown in Listing 19-4. Figure 19-3 shows what the user sees.

Listing 19-4. Code for the MapFragment

public class MyMapFragment extends SupportMapFragment
    implements OnMapReadyCallback {
    private GoogleMap mMap = null;

    public static MyMapFragment newInstance() {
        MyMapFragment myMF = new MyMapFragment();
        return myMF;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getMapAsync(this);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onResume() {
        super.onResume();
        doWhenMapIsReady();
    }

    @Override
    public void onPause() {
        super.onPause();
        if(mMap != null)
            mMap.setMyLocationEnabled(false);
    }

    @Override
    public void onMapReady(GoogleMap arg0) {
        mMap = arg0;
        doWhenMapIsReady();
    }

    /* We have a race condition where the fragment could resume
     * before or after the map is ready. So we put all our logic
     * for initializing the map into a common method that is
     * called when the fragment is resumed or resuming and the
     * map is ready.
     */
    void doWhenMapIsReady() {
        if(mMap != null && isResumed())
            mMap.setMyLocationEnabled(true);
    }
}

9781430246800_Fig19-03.jpg

Figure 19-3. A basic MapFragment displaying your location

A recent development (Dec 2014) to the Maps API is the use of a callback to let the application know when the map is ready to be acted upon. The callback is set up using getMapAsync(), and the onMapReady() callback is called when the map can be used by the application. In between calling getMapAsync() and invoking the onMapReady() callback, Android is setting up communications, threads, etc., for the map. This means that the map may or may not be ready when onResume() is invoked, which tells the fragment that the UI is now being shown. Therefore, the application needs a separate method to work on the map and it needs to be called both by onResume() and by onMapReady(). For this sample application, the doWhenMapIsReady() method fills that role.

The application wants to show the user the device’s current location, so the setMyLocationEnabled() method is called in doWhenMapIsReady(). But doWhenMapIsReady() needs to check that the map exists and that the fragment is resuming or has resumed. We don’t know which will occur first, but both must be true before we enable location updates. The current location updates are disabled when the fragment goes out of view (see onPause()). The only other code line to notice is the setRetainInstance() method call. Since the map does not need to be destroyed and re-created for a configuration change of the activity, it makes sense to keep the fragment and reuse it, along with the threads and tiles and so on. You should remember that a configuration change will cause onPause() and onResume() to be invoked during the config change. This will correctly disable location updates and re-enable them during onResume().

Map Controls: MyLocation, Zoom, Pan

There are a couple of artifacts on the user interface to notice. First is the MyLocation button in the upper-right corner. When you first start the sample app you will see a very high-level view of the world. To show the current location, tap the MyLocation button. This will relocate the map to the current position and zoom in. Second is the blue dot. The blue dot represents where the app thinks you are, and the circle represents how accurate it thinks this location is. The circle may grow or shrink as the location information changes.

The user can use pinch gestures (i.e., squeezing two fingers apart or together) to zoom in or out. There are more gestures that the user can do on the map. By swiping, the user can pan the map; that is, they can move the map to see nearby areas. Using two fingers and a rotation move, the user can rotate the map. That’s a lot of functionality that’s automatic from simply creating a MapFragment.

These map controls and more are contained in an object of the UiSettings class. You can get the map’s UiSettings by calling getUiSettings() on the GoogleMap object (i.e., mMap in the sample app). You can then modify the settings programmatically. For example, you could enable a compass to be displayed on the map, or you could enable/disable the zoom plus/minus control so it is or is not displayed. The zoom plus/minus control appears in the lower-right corner and allows the user to zoom in or out by tapping the plus or minus button, respectively.

Map Types

The default map type is MAP_TYPE _NORMAL. This is the type that was used in Figure 19-3. It shows the roads with the basic features of the land such as where water is, where greenspace is, and some places and buildings. MAP_TYPE_SATELLITE shows a photographic satellite view of the ground, so the user is able to see actual buildings, cars, and even people. MAP_TYPE_HYBRID is a combination of these two; MAP_TYPE_TERRAIN is like a normal map but with topographical features added such as mountains and canyons. To really see the effect of MAP_TYPE_TERRAIN, zoom in on a place like Boulder, Colorado with a map set to Terrain.

You use the setMapType() method of a GoogleMap to change the type.

Adding a Traffic Layer

In the previous version of Android Maps, traffic was treated like the satellite and normal modes of maps. With API v2, traffic is enabled separately using the setTrafficEnabled() method of a GoogleMap.

Map Tiles

It’s helpful to understand what’s going on when your app displays a map. Google has created millions of base map tiles to represent the earth’s surface. At the lowest zoom level (i.e., zero) there is one tile to show the entire world. At zoom level 1, there are four tiles in a 2x2 configuration. At zoom level 2, there are 16 tiles in a 4x4 configuration. And so on up to zoom level 21. Depending on what part of the world you want to display, and what the zoom level is, the GoogleMap object will fetch and cache the appropriate tiles. Pan to the side, and any additional tiles will be fetched and displayed. Pan back to where you were and your app can retrieve map tiles from the cache instead of making more round trips to the server.

It’s interesting to note that base map tiles for the normal type of maps are not images. Google has figured out a compressed way to describe the shapes and colors within the tiles instead of just sending down images for each tile. As a result, normal map tiles are very efficient in terms of cache space as well as network bandwidth. Satellite tiles on the other hand are not as compressed, since they are images.

Now you can understand why sometimes a maps application will show a gray grid pattern and seem to function but won’t show streets and other items. The GoogleMap object has been instantiated, and it knows a zoom level and where it is supposed to be displaying a map, but it is unable to retrieve and render tiles to the user. This is most often due to an invalid Maps API key, or the API key has not been set up properly. But it could also mean difficulty in reaching the Google Maps servers. However, if map tiles have been cached, those tiles can be rendered to the user even if the tile servers at Google are unreachable. There are two unfortunate things about map tile caching. The first is that there are no API calls to manage the map tile cache, either to force map tiles to be cached, or to change the size of the cache, or to evict tiles from the cache. You just have to trust that Google will do the right thing. The second is that map tiles are cached per application. So just because the Google Maps application may have cached tiles, your application does not have access to those tiles. Your application can only see the cached tiles that it has cached.

Adding Markers to Maps

Usually you’ll want to identify points of interest on a map, and this is done using Markers. The points could be stationary objects like addresses, landmarks, or a parking spot. But they could also be moving objects like cars, planes, people, pets, storms, etc. You get to choose what the marker looks like and where it is positioned on a map. And you can have lots of markers all at the same time. We’re going to modify the sample program from above to include a couple of markers. You’ll see how to place them, and then how to manipulate the view to make sure the user sees the markers.

Now use the sample program called WhereAmIMarkers. You will need to modify the AndroidManifest.xml file as before to use your Maps API key. The source code for MyMapFragment.java has been modified as shown in Listing 19-5. The screen will appear similar to Figure 19-4.

Listing 19-5. Code for the MapFragment Showing Markers

public class MyMapFragment extends SupportMapFragment
    implements OnMapReadyCallback {

    public static MyMapFragment newInstance() {
        MyMapFragment myMF = new MyMapFragment();
        return myMF;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getMapAsync(this);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onMapReady(GoogleMap myMap) {
        LatLng disneyMagicKingdom = new LatLng(28.418971, -81.581436);
        LatLng disneySevenLagoon = new LatLng(28.410067, -81.583699);

        // Add a marker
        MarkerOptions markerOpt = new MarkerOptions()
                .draggable(false)
                .flat(false)
                .position(disneyMagicKingdom)
                .title("Magic Kingdom")
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));
        myMap.addMarker(markerOpt);

        markerOpt.position(disneySevenLagoon)
                 .title("Seven Seas Lagoon");
        myMap.addMarker(markerOpt);

        // Derive a bounding box around the markers
        LatLngBounds latLngBox = LatLngBounds.builder()
                .include(disneyMagicKingdom)
                .include(disneySevenLagoon)
                .build();

        // Move the camera to zoom in on our locations
        myMap.moveCamera(CameraUpdateFactory.newLatLngBounds(latLngBox, 200, 200, 0));
    }
}

9781430246800_Fig19-04.jpg

Figure 19-4. Markers on a map

Once again, everything starts from acquiring the GoogleMap object from the MapFragment. Once the map is available, you can create markers which in this case are based on a couple of fixed LatLng objects. You’ll notice though that you don’t directly instantiate a Marker object. Instead, you use a MarkerOptions object to specify how the marker should be created. It is within the MarkerOptions object that you decide the position, title, marker shape, color, etc. While you could instantiate a Marker object and then call each setter that you want, MarkerOptions makes things much easier, especially if you need to create multiple markers that will share common features. This sample only uses some of the MarkerOptions features; please see the reference documentation to learn all of the options available.

The next thing you likely want to do is show the map to the user such that all the markers are visible at the same time. This requires two things: centering the map in the middle of the markers, and setting the zoom level as high as possible without being so close that you can’t fit all the markers into the view. Fortunately, a helper class is available for this purpose. The LatLngBounds object is created by passing it all of the LatLng points that should be within the view, and it calculates the smallest box that contains them all. In this sample, both points are passed in at once. You could also use a loop to pass in all the points and then invoke the build() method to return the bounding box.

Once you have a bounding box, you need to adjust the map’s camera. In the old version of Google Maps, there was only a straight-down view of a map, as if you were above the map looking straight down. With Maps API version 2, there is the concept of a camera that can look straight down, but can also look at an angle. If you use two fingers at the same time and swipe the screen from top to bottom, you will see the viewing angle change. You have in effect pivoted the camera so you are no longer looking straight down. The camera can also look to the east, south or any other direction when it is angled. You can twist two fingers to rotate the map too.

All these camera angles, zoom levels and so on are controlled using the map’s animateCamera() or moveCamera() methods. These methods take a CameraUpdate object as instructions, and the CameraUpdateFactory class generates those. In the sample, the bounding box is passed to the CameraUpdateFactory and it returns an appropriate CameraUpdate so that the camera will be positioned in the best place to see all the markers. There are several other methods to CameraUpdateFactory to accommodate other ways of positioning the camera. You do can simple zoomIn() and zoomOut() for example. You can also create a CameraPosition object and use that.

All in all, you’ll agree that placing markers on a map couldn’t be easier. Or could it? We don’t have a database of latitude/longitude pairs, but we’re guessing that we’ll need to somehow create one or more LatLng objects using a real address. That’s when you can use the Geocoder class, which is part of the location package that we’ll discuss next.

Understanding the Location Package

The android.location package provides facilities for location-based services. In this section, we are going to discuss two important pieces of this package: the Geocoder class and the LocationManager service. We’ll start with Geocoder.

Geocoding with Android

If you are going to do anything practical with maps, you’ll likely have to convert an address (or location) to a latitude/longitude pair. This concept is known as geocoding, and the android.location.Geocoder class provides this facility. In fact, the Geocoder class provides both forward and backward conversion—it can take an address and return a latitude/longitude pair, and it can translate a latitude/longitude pair into a list of addresses. The class provides the following methods:

  • List<Address>   getFromLocation(double latitude, double longitude, int maxResults)
  • List<Address>   getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude)
  • List<Address>   getFromLocationName(String locationName, int maxResults)

It turns out that computing an address is not an exact science because of the various ways a location can be described. For example, the getFromLocationName() methods can take the name of a place, the physical address, an airport code, or simply a well-known name for the location. Thus, the methods return a list of addresses and not a single address. Because the methods return a list, which could be quite long (and take a long time to return), you are encouraged to limit the result set by providing a value for maxResults that ranges between 1 and 5. Now, let’s consider an example.

Listing 19-6 shows the XML layout and corresponding code for the activity and map fragment shown in Figure 19-5. To run the example, you’ll need to update the manifest with your own Maps API key.

Listing 19-6. Working with the Android Geocoder Class

<!-- This is activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.androidbook.maps.whereami.MainActivity"
    tools:ignore="MergeRootFrame" >

    <EditText android:id="@+id/locationName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter location name"
        android:inputType="text"
        android:imeOptions="actionGo" />

    <FrameLayout android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

/**
  * This is from MainActivity.java
 **/
public class MainActivity extends FragmentActivity {

    private static final String MAPFRAGTAG = "MAPFRAGTAG";
    MyMapFragment myMapFrag = null;
    private Geocoder geocoder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if ((myMapFrag = (MyMapFragment) getSupportFragmentManager()
                         .findFragmentByTag(MAPFRAGTAG)) == null) {
            myMapFrag = MyMapFragment.newInstance();
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, myMapFrag, MAPFRAGTAG).commit();
        }
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD
                        && !Geocoder.isPresent()) {
            Toast.makeText(this, "Geocoder is not available on this device",
                       Toast.LENGTH_LONG).show();
            finish();
        }
        geocoder = new Geocoder(this);
        EditText loc = (EditText)findViewById(R.id.locationName);
        loc.setOnEditorActionListener(new OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_GO) {
                    String locationName = v.getText().toString();

                    try {
                        List<Address> addressList =
                           geocoder.getFromLocationName(locationName, 5);
                        if(addressList!=null && addressList.size()>0)
                        {
//                       Log.v(TAG, "Address: " + addressList.get(0).toString());
                            myMapFrag.gotoLocation(new LatLng(
                                addressList.get(0).getLatitude(),
                                addressList.get(0).getLongitude()),
                                locationName);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return false;
            }
        });
    }
}

public class MyMapFragment extends SupportMapFragment
    implements OnMapReadyCallback {
    private GoogleMap mMap = null;

    public static MyMapFragment newInstance() {
        MyMapFragment myMF = new MyMapFragment();
        return myMF;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getMapAsync(this);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setRetainInstance(true);
    }

    public void gotoLocation(LatLng latlng, String locString) {
        if(mMap == null)
            return;
        // Add a marker for the given location
        MarkerOptions markerOpt = new MarkerOptions()
                .draggable(false)
                .flat(false)
                .position(latlng)
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
                .title("You chose:")
                .snippet(locString);
        // See the onMarkerClicked callback for why we do this
        mMap.addMarker(markerOpt);

        // Move the camera to zoom in on our location
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latlng, 15));
    }

    @Override
    public void onMapReady(GoogleMap arg0) {
        mMap = arg0;
    }
}

9781430246800_Fig19-05.jpg

Figure 19-5. Geocoding to a point given the location name

To demonstrate the uses of geocoding in Android, type the name or address of a location in the EditText field and then tap the Go button on the keyboard. To find the address of a location, we call the getFromLocationName() method of Geocoder. The location can be an address or a well-known name such as “White House.” Geocoding can be a prolonged operation, so we recommend that you limit the results to five, as the Android documentation suggests.

The call to getFromLocationName() returns a list of addresses. The sample application takes the list of addresses and processes the first one if any were found. Every address has a latitude and longitude, which you use to create a LatLng. You then call our gotoLocation() method to navigate to the point. This new method in the map fragment creates a new marker, adds it to the map, and moves the camera to the marker with a zoom level of 15. The zoom level can be set to a float between 1 and 21, inclusive. As you move from 1 toward 21 by 1’s, the zoom level increases by a factor of 2. We could have presented a dialog to display multiple found locations if we wanted to, but for now, we’ll just display the first location returned to us.

In our example application, we only read the latitude and longitude of our returned Address. In fact, there can be a ton of data about Addresses returned to us, including the location’s common name, street, city, state, postal/ZIP code, country, and even phone number and web site URL.

You should understand a few points with respect to geocoding:

  • While the Geocoder class may exist, the service may not be implemented. If the device is Gingerbread or higher, you should check with Geocoder.isPresent() before attempting to geocode in your application.
  • A returned address is not always an exact address. Obviously, because the returned list of addresses depends on the accuracy of the input, you need to make every effort to provide an accurate location name to the Geocoder.
  • Whenever possible, set the maxResults parameter to a value between 1 and 5.
  • You should seriously consider doing the geocoding operation in a different thread from the UI thread. There are two reasons for this. The first is obvious: the operation is time-consuming, and you don’t want the UI to hang while you do the geocoding, causing Android to kill your activity. The second reason is that, with a mobile device, you always need to assume that the network connection can be lost and that the connection is weak. Therefore, you need to handle input/output (I/O) exceptions and timeouts appropriately. Once you have computed the addresses, you can post the results to the UI thread. See the included sample application called WhereAmIGeocoder2 for how to do this.

Understanding Location Services

Location Services provide two main functions: a mechanism for you to obtain the device’s geographical location and a facility for you to be notified (via an intent) when the device enters or exits a specified geographical location. This latter operation is known as geofencing.

In this section, you are going to learn how to find the device’s current location. To use the service, you must first obtain a reference to it. Listing 19-7 shows a simple usage of the FusedLocationProviderApi service. The sample project for this is called WhereAmILocationAPI.

Listing 19-7. Using the Location Provider API

public class MyMapFragment extends SupportMapFragment
    implements GoogleApiClient.ConnectionCallbacks,
               GoogleApiClient.OnConnectionFailedListener,
               OnMapReadyCallback {
    private Context mContext = null;
    private GoogleMap mMap = null;
    private GoogleApiClient mClient = null;
    private LatLng mLatLng = null;

    public static MyMapFragment newInstance() {
        MyMapFragment myMF = new MyMapFragment();
        return myMF;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getMapAsync(this);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if(mClient == null) { // first time in, set up this fragment
            setRetainInstance(true);

            mContext = getActivity().getApplication();
            mClient = new GoogleApiClient.Builder(mContext, this, this)
                .addApi(LocationServices.API)
                .build();
            mClient.connect();
        }
    }

    @Override
    public void onConnectionFailed(ConnectionResult arg0) {
        Toast.makeText(mContext, "Connection failed", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onConnected(Bundle arg0) {
        // Figure out where we are (lat, long) as best as we can
        // based on the user's selections for Location Settings
        FusedLocationProviderApi locator = LocationServices.FusedLocationApi;
        Location myLocation = locator.getLastLocation(mClient);
        // if the services are not available, could get a null location
        if(myLocation == null)
            return;
        double lat = myLocation.getLatitude();
        double lng = myLocation.getLongitude();
        mLatLng = new LatLng(lat, lng);
        doWhenEverythingIsReady();
    }

    @Override
    public void onConnectionSuspended(int arg0) {
        Toast.makeText(mContext, "Connection suspended", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onMapReady(GoogleMap arg0) {
        mMap = arg0;
        doWhenEverythingIsReady();
    }

    private void doWhenEverythingIsReady() {
        if(mMap == null || mLatLng == null)
            return;
        // Add a marker
        MarkerOptions markerOpt = new MarkerOptions()
                .draggable(false)
                .flat(true)
                .position(mLatLng)
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));
        mMap.addMarker(markerOpt);

        // Move the camera to zoom in on our location
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(mLatLng, 15));
    }
}

To acquire a location service, you first need to create a Google API client object, which makes available to you the services from Google Play Services. This is relatively easy to do, and once you have the client object, you need to call its connect() method. This will later invoke the onConnected() callback asynchronously to let your application know that the client has been connected and is now available for use. Or your application may get the onConnectionFailed() callback, in which case you should take appropriate action. For the sample we simply show a Toast message when the connection attempt has failed. Later on you’ll see how to deal more robustly with a failed connection.

When the onConnected() callback is invoked, now you can work with the location provider API. Recall that at the beginning of this chapter you set permissions in the manifest file to access location information. Fine locations use GPS while coarse locations use cell towers and WiFi hotspots. Using a fused location provider API means that your application isn’t worrying about what is enabled or what permissions are set. The API calls are the same. You just ask for locations and you will get the best location information that is available at the time.

For this sample, we call the getLastLocation() method. With luck, the location that is returned is very current; however, be aware that the last location might be from minutes or hours ago. The Location object can tell you, via the getTime() method, when this location fix was obtained. You could check to see if it is new enough for your purposes before deciding to use it. It is technically possible that getLastLocation() will return null so you should be prepared for that case as well. This can happen if Location Services have been disabled in Settings.

You’ll see soon how to get updates to locations. For now, the sample takes whatever the last location was and creates a map marker out of it for display to the user. You should recognize the code to create a marker from the previous section of this chapter.

How to Enable Location Services

You might think there’s a simple API to enable Location Services if they are not turned on when your application runs. Unfortunately, this is not the case. To get Location Services turned on, the user must do that from within the Settings screens of their device. Your application can make this a lot simpler for the user by launching that particular Settings screen. The location settings source screen is really just an activity, and this activity is set up to respond to an intent.

In the sample application just covered, you will see the code from Listing 19-8 in the activity’s onCreate() callback.

Listing 19-8. Checking to See If Location Services Are On

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if ((myMapFrag = (MyMapFragment) getSupportFragmentManager()
            .findFragmentByTag(MAPFRAGTAG)) == null) {
        myMapFrag = MyMapFragment.newInstance();
        getSupportFragmentManager().beginTransaction()
            .add(R.id.container, myMapFrag, MAPFRAGTAG).commit();
    }

    if(!isLocationEnabled(this)) {
        // no location service providers are enabled
        Toast.makeText(context, "Location Services appear to be turned off." +
            " This app can't work without them. Please turn them on.",
            Toast.LENGTH_LONG).show();
        startActivityForResult(new Intent(
            android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0);
    }
}

@SuppressWarnings("deprecation")
public boolean isLocationEnabled(Context context) {
    int locationMode = Settings.Secure.LOCATION_MODE_OFF;
    String locationProviders;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
        try {
            locationMode = Settings.Secure.getInt(
                context.getContentResolver(),
                Settings.Secure.LOCATION_MODE);
        } catch (SettingNotFoundException e) {
            e.printStackTrace();
        }
        return locationMode != Settings.Secure.LOCATION_MODE_OFF;
    }else{
        locationProviders = Settings.Secure.getString(
            context.getContentResolver(),
            Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
        return !TextUtils.isEmpty(locationProviders);
    }
}

A change occurred in Android 19 (KitKat) where new settings values were added to the static Settings.Secure class. This made it easier to tell if Location Services were turned on or not, and which ones, but the user still needs to do the work to enable the services. There are two ways in this code to check for services: use one of the new values, or do a get on the available location providers. The first part of Listing 19-8 checks to see if the version of Android is KitKat or higher, and if so it looks for the value of the new Setting for location mode. The second part of the code (if the version of Android is older than KitKat) does a get on the allowed location providers. If location mode is not off, or if there is at least one location provider available, then Location Services are running. If not, this code launches an intent to the Location Settings screen. At that point, this activity would be paused while the Settings activity runs. When the Settings activity is done, our activity will resume.

If you want to handle a response from the Settings activity (i.e., be notified when that activity is done and presumably a setting change has been made), you must implement the onActivityResult() callback in your activity. And also keep in mind that although you hope the user turns on location services, they may not. You will need to check again to see if the user has enabled location services and take appropriate action based on the result. We’ll show you how to do all of this in a later section.

Location Providers

You’ve seen the FusedLocationApi, but you should also be aware of the older, alternate location providers. The hardware is right there on the device for getting location information, and the location providers will give it to your application. You’ll soon see how the FusedLocationApi handles your location needs at a higher level than these providers. But if you need to dig into the details, for example to check the status of the available GPS satellites, you’ll be happy to know these providers exist. Google recommends that everyone switch over to the FusedLocationApi; but since it relies on Google Play Services, that means applications that use FusedLocationApi will not run on a non-Google Android device.

The LocationManager service is a system-level service. System-level services are services that you obtain from the context using the service name; you don’t instantiate them directly. The android.app.Activity class provides a utility method called getSystemService() that you can use to obtain a system-level service. You call getSystemService() and pass in the name of the service you want, in this case, Context.LOCATION_SERVICE. You’ll see this shortly in Listing 19-9.

The LocationManager service provides geographical location details by using location providers. Currently, there are three types of location providers:

  • GPS providers use a Global Positioning System to obtain location information.
  • Network providers use cell-phone towers or WiFi networks to obtain location information.
  • The passive provider is like a location update sniffer, and it passes to your application location updates that are requested by other applications, without your application having to specifically request any location updates. Of course, if no one else is requesting location updates, you won’t get any either.

Similar to the FusedLocationApi, the LocationManager class can provide the device’s last known location, this time via the getLastKnownLocation() method. Location information is obtained from a provider, so the method takes as a parameter the name of the provider you want to use. Valid values for provider names are LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, and LocationManager.PASSIVE_PROVIDER. Note that there is no option for a fused provider, since that is a separate location-finding capability.

In order for your application to successfully get location information, it must have the appropriate permissions in the AndroidManifest.xml file. android.permission.ACCESS_FINE_LOCATION is required for GPS and for passive providers, whereas android.permission.ACCESS_COARSE_LOCATION or android.permission.ACCESS_FINE_LOCATION can be used for network providers, depending on what you need. For instance, assume your application will use GPS or network data for location updates. Because you need ACCESS_FINE_LOCATION for GPS, you’ve also satisfied permissions for network access, so you do not need to also specify ACCESS_COARSE_LOCATION. If you’re only going to use the network provider, you could get by with only ACCESS_COARSE_LOCATION in the manifest file.

Calling getLastKnownLocation() returns an android.location.Location instance, or null if no location is available. The Location class provides the location’s latitude and longitude, the time the location was computed, and possibly the device’s altitude, speed, and bearing. A Location object can also tell you which provider it came from using getProvider(), which will be either GPS_PROVIDER or NETWORK_PROVIDER. If you’re getting location updates via the PASSIVE_PROVIDER, remember that you’re only really sniffing location updates, so all updates are ultimately from either GPS or the network.

Because the LocationManager operates on providers, the class provides APIs to obtain providers. For example, you can get all of the known providers by calling getAllProviders(). You can obtain a specific provider by calling getProvider(), passing the name of the provider as an argument (such as LocationManager.GPS_PROVIDER). One thing to watch out for is that getAllProviders() will return providers that you may not have access to or that are currently disabled. Fortunately, you are able to determine the status of providers using other methods, such as isProviderEnabled(String providerName) or getProviders(boolean enabledOnly), which you could call with a value of true to get only providers you are able to use immediately.

There’s another way to get a suitable provider, and that is to use the getProviders(Criteria criteria, boolean enabledOnly) method of LocationManager. By specifying criteria for location updates, and by setting enabledOnly to true so you get providers that are enabled and ready to go, you can get a list of provider names returned to you without having to know the specifics of which provider you got. This could be more portable, because a device may have a custom LocationProvider that meets your needs without you having to know about it in advance. The Criteria object can be set with parameters that include accuracy level and the need for information about speed, bearing, altitude, cost, and power requirements. If no providers meet your criteria, a null list will be returned, allowing you to either bail out or relax the criteria and try again.

Sending Location Updates to Your Application

When doing development testing, your application needs location information, and the emulator doesn’t have access to GPS or cell towers. In order for you to test your application in the emulator, you can manually send location updates from Eclipse. Listing 19-9 shows a simple example to illustrate how to do this. We’re going to stick with the LocationManager approach here, and then show the FusedLocationApi approach later.

Listing 19-9. Registering for Location Updates

public class LocationUpdateDemoActivity extends Activity
{
    LocationManager locMgr = null;
    LocationListener locListener = null;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        locMgr = (LocationManager)
            getSystemService(Context.LOCATION_SERVICE);

        locListener = new LocationListener()
        {
            public void  onLocationChanged(Location location)
            {
                if (location != null)
                {
                    Toast.makeText(getBaseContext(),
                        "New location latitude [" +
                        location.getLatitude() +
                        "] longitude [" +
                        location.getLongitude()+"]",
                        Toast.LENGTH_SHORT).show();
                }
            }

            public void  onProviderDisabled(String provider)
            {
            }

            public void  onProviderEnabled(String provider)
            {
            }

            public void  onStatusChanged(String provider,
                            int status, Bundle extras)
            {
            }        };
    }

    @Override
    public void onResume() {
        super.onResume();

        locMgr.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            0,                // minTime in ms
            0,                // minDistance in meters
            locListener);
    }

    @Override
    public void onPause() {
        super.onPause();
        locMgr.removeUpdates(locListener);
    }
}

We’re not displaying a user interface for this example, so the standard initial layout XML file will do, as well as a regular activity.

One of the primary uses of the LocationManager service is to receive notifications of the device’s location. Listing 19-9 demonstrates how you can register a listener to receive location-update events. To register a listener, you call the requestLocationUpdates() method, passing the provider type as one of the parameters. When the location changes, the LocationManager calls the onLocationChanged() method of the listener with the new Location. It is very important that you remove any registrations for location updates at the appropriate time. In our example, we do registration in onResume(), and we remove that registration in onPause(). If we aren’t going to be around to do anything with location updates, we should tell the provider not to send them. There’s also the possibility that our activity could be destroyed (for example, if the user rotates their device and our activity is restarted), in which case our old activity could still exist, be receiving updates, displaying them with Toast, and taking up memory.

In our example, we set the minTime and minDistance to zero. This tells the LocationManager to send us updates as often as possible. These are not desired settings for your production application, or on real devices, but we use them here to make the demonstrations run better in the emulators. (In real life, you would not want the hardware trying to figure out our current position so often, as this drains the battery.) Set these values appropriately for the situation, trying to minimize how often you truly need to be notified of a change in position. Google typically recommends values no smaller than 20 seconds.

Testing Location Applications with the Emulator

Let’s test this in the emulator, using the Dalvik Debug Monitor Service (DDMS) perspective that ships with the ADT plug-in for Eclipse. The DDMS UI provides a screen for you to send the emulator a new location (see Figure 19-6).

9781430246800_Fig19-06.jpg

Figure 19-6. Using the DDMS UI in Eclipse to send location data to the emulator

To get to the DDMS in Eclipse, use Window image Open Perspective image DDMS. The Emulator Control view should already be there for you, but if not, use Window image Show View image Other image Android image Emulator Control to make it visible in this perspective. You may need to scroll down in the emulator control to find the location controls. As shown in Figure 19-6, the Manual tab in the DDMS user interface allows you to send a new GPS location (latitude/longitude pair) to the emulator. Sending a new location will fire the onLocationChanged() method on the listener, which will result in a message to the user conveying the new location.

You can send location data to the emulator using several other techniques, as shown in the DDMS user interface (see Figure 19-6). For example, the DDMS interface allows you to submit a GPS Exchange Format (GPX) file or a Keyhole Markup Language (KML) file. You can obtain sample GPX files from these sites:

Similarly, you can use the following KML resources to obtain or create KML files:

Note  Some sites provide KMZ files. These are zipped KML files, so simply unzip them to get to the KML file. Some KML files need to have their XML namespace values altered in order to play properly in DDMS. If you have trouble with a particular KML file, make sure it has this:

<kml xmlns="http://earth.google.com/kml/2.x">.

You can upload a GPX or KML file to the emulator and set the speed at which the emulator will play back the file (see Figure 19-7). The emulator will then send location updates to your application based on the configured speed. As Figure 19-7 shows, a GPX file contains points, shown in the top part, and paths, shown in the bottom part. You can’t play a point, but when you click a point, it will be sent to the emulator. You click a path, and then the Play button will be enabled so you can play the points.

9781430246800_Fig19-07.jpg

Figure 19-7. Uploading GPX and KML files to the emulator for playback

Note  There have been reports that not all GPX files are understandable by the emulator control. If you attempt to load a GPX file and nothing happens, try a different file from a different source.

Listing 19-9 includes some additional methods for LocationListener we haven’t mentioned yet. They are the callbacks onProviderDisabled(), onProviderEnabled(), and onStatusChanged(). For our sample, we did not do anything with these, but in your application, you could be notified when a location provider, such as gps, is disabled or enabled by the user, or when a status changes with one of the location providers. Statuses include OUT_OF_SERVICE, TEMPORARILY_UNAVAILABLE, and AVAILABLE. Even if a provider is enabled, it does not mean that it will be sending any location updates, and you can tell that using statuses. Note that onProviderDisabled() will be invoked immediately if a requestLocationUpdates() is called for a disabled provider.

Sending Location Updates from the Emulator Console

Eclipse has some easy-to use-tools for sending location updates to your application, but there’s another way to do it. You could launch the emulator console, using the following command from a tools window:

telnet localhost emulator_port_number

where emulator_port_number is the number associated to the instance of the AVD that’s already running, displayed in the title bar of the emulator window. You may need to install telnet for your workstation if it’s not already available. Once you’re connected, you can use the geo fix command to send in location updates. To send in latitude/longitude coordinates with altitude (altitude is optional), use this form of the command:

geo fix lon lat [ altitude ]

For example, the following command will send the location of Jacksonville, Florida to your application with an altitude of 120 meters.

geo fix  -81.5625  30.334954  120

Please pay careful attention to the order of the arguments to the geo fix command. Longitude is the first argument, and latitude is the second.

What Can You Do with a Location?

As mentioned before, Locations can tell you the latitude and longitude, when the Location was computed, the provider that computed this Location, and optionally the altitude, speed, bearing, and accuracy level. Depending on the provider where the Location came from, there could be extra information as well. For example, if the Location came from a GPS provider, there is an extras Bundle that will tell you how many satellites were used to compute the Location. The optional values may or may not be present, depending on the provider. To know if a Location has one of these values, the Location class provides a set of has...() methods that return a boolean value, for example hasAccuracy(). Before relying on the return value of getAccuracy(), it would be wise to call hasAccuracy() first.

The Location class has some other useful methods, including a static method distanceBetween(), which will return the shortest distance between two Locations. Another distance-related method is distanceTo(), which will return the shortest distance between the current Location object and the Location object passed to the method. Note that distances are in meters and that the distance calculations take into account the curvature of the Earth. But also be aware that the distances are not provided in terms of the distance you would have to go by car, for example.

If you want to get driving directions or driving distances, you will need to have your beginning and ending Locations, but to do the calculations, you will likely need to use the Google Directions API. The Directions API would allow your application to show how to get from your beginning to your ending location. This is another of the Google API client APIs that you can enable for your application.

Setting Up for Google Play Services Location Updates

You’ve seen how to get location updates with a LocationManager, but let’s return to the FusedLocationProviderApi to see how to get location updates from it. The sample project for this section is FusedLocationApiUpdates. This one is a bit trickier because we are dealing with Google Play Services, an independent service running on the device. Therefore, you can’t always be sure that you have a valid client connection, and you need to be careful when requesting location updates. For this reason, your application will need to worry about state.

In the earlier sample program (WhereAmILocationAPI), you checked to see if Location Services were turned on, but the code assumed that Google Play Services were available and ready. Now you’re going to see how to check for the existence of Google Play Services and how the GooglePlayServicesUtil class can help you. The basic flow is to check each dependency for location updates to occur and, if there is a way to correct a problem, help the user fix it. If the user does not, or cannot, fix a problem, the application quits. If the user keeps fixing problems until everything is working, then location updates get requested, and the application displays location updates via Toast messages.

Listing 19-10 shows our main method for trying to connect. You will see inside this method the same Location Services check from the earlier WhereAmILocationAPI sample application. The tryToConnect() method will be called from the activity’s onResume() callback, so that every time this activity is resumed, a new client connection will be established to Google Play Services. We do not want to assume that an old client is still valid and active.

Listing 19-10. Checking for the Ability to Do Location Updates

private void tryToConnect() {
    // Check that Google Play services is available
    int resultCode = GooglePlayServicesUtil
                    .isGooglePlayServicesAvailable(this);
    // If Google Play services is available, then we're good
    if (resultCode == ConnectionResult.SUCCESS) {
        Log.d(TAG, "Google Play services is available.");
        if(!isLocationEnabled(this)) {
            if(lastFix == FIX.LOCATION_SETTINGS) {
                // Since we're coming through again, it means
                // recovery didn't happen. Time to bail out.
                Log.e(TAG, "Location settings didn't work");
                finish();
            }
            else {
                // no location service providers are enabled
                Toast.makeText(this, "Location Services are off. " +
                    "Can't work without them. Please turn them on.",
                    Toast.LENGTH_LONG).show();
                Log.i(TAG, "Location Services need to be on. " +
                    "Launching the Settings screen");
                startActivityForResult(new Intent(
                    android.provider.Settings
                        .ACTION_LOCATION_SOURCE_SETTINGS),
                    LOCATION_SETTINGS_REQUEST);
                lastFix = FIX.LOCATION_SETTINGS;
            }
        }
        else {
            client.connect();
            Log.v(TAG, "Connecting to GoogleApiClient...");
        }
    }
    // Google Play services was not available for some reason
    // See if the user can do something about it
    else if(GooglePlayServicesUtil
                .isUserRecoverableError(resultCode)) {
        if(lastFix == FIX.PLAY_SERVICES) {
            // Since we're coming through again, it means
            // recovery didn't happen. Time to bail out.
            Log.e(TAG, "Recovery doesn't seem to work");
            finish();
        }
        else {
            Log.d(TAG, "Google Play services may be available. " +
                "Asking user for help");
            // This form of the dialog call will result in either a
            // callback to onActivityResult, or a dialog onCancel.
            GooglePlayServicesUtil.showErrorDialogFragment(resultCode,
                this, PLAY_SERVICES_RECOVERY_REQUEST, this);
            lastFix = FIX.PLAY_SERVICES;
        }
    } else {
        // No hope left.
        Log.e(TAG, "Google Play Services is/are not available." +
              " No point in continuing");
        finish();
    }
}

The GooglePlayServicesUtil class has several static methods to help get everything set up for location updates. The first method is isGooglePlayServicesAvailable(), which requires a context. The result is an integer value which is either SUCCESS or one of several other values which could indicate for example that the services are missing, or the version is not appropriate. For most purposes, you don’t really need to care about the other values that are returned, as you’ll see.

If Google Play Services are available, you will check for Location Services (as before) and if they are okay, you can invoke the connect() method on the GoogleApiClient client. The connect() call is asynchronous and a separate callback will handle the results of the connect call. As before, if Location Services are not turned on, you would launch the location settings activity so the user could turn them on. In this sample, we just use a Toast message to tell the user why they are being redirected to the Settings screen. In a production application, you would probably want to show an alert dialog with an OK and Cancel button before redirecting to the Settings screen.

If Google Play Services are not available, the next check is to see if the user could resolve the issue, using the isUserRecoverableError() method. Here you pass in the result code from the earlier check, which should be something other than SUCCESS. This is why you don’t need to care what other value was returned. This method decides for you if the user can do something about it or not. If the user can’t correct the situation (i.e., isUserRecoverableError() returns false), then there really isn’t anything else you can do and you will probably want to bail out. In this sample application a log message is written and the activity ends. You might want to be more graceful in your exit.

If the user can do something about the problem with Google Play Services, the GooglePlayServicesUtil class has yet another static method you can use: showErrorDialogFragment(). This will show a dialog to the user indicating what the problem is and what they can do about it. There are a few variations on this call, and the sample is using the one which pops a dialog fragment while listening for a dialog cancel. The dialog fragment could launch another activity, which would result in our onActivityResult() being called. For this reason, you want to pass in a request value (i.e., PLAY_SERVICES_RECOVERY_REQUEST), which will be passed to onActivityResult() later. This method is also asynchronous, and your application will see either onActivityResult() invoked later, or the onCancel() for the dialog. The second argument to showErrorDialogFragment() is the context, and the last argument is the listener for the dialog. Because we passed 'this' as the last argument, to represent this activity, the sample activity must implement DialogInterface.OnCancelListener and have an onCancel() callback.

You’ll soon see the code for onActivityResult(), but you should know that when a result is passed back to your activity, you’re going to have do these checks again, by calling tryToConnect(). That is why this method sets a lastFix value, to keep track of which problem is being worked on. If the same problem exists after the user has had a chance to fix it, we could assume that the user isn’t interested in fixing the problem, or the system is unable to fix the problem. We do not want some sort of infinite loop that the user cannot break out of. For this sample activity, if tryToConnect() hits the same problem twice in a row, it bails out and the activity is finished. Your application might want to take alternative action, giving the user more options to continue to use the app.

To recap what has happened in tryToConnect(), you checked for the existence and readiness of Google Play Services, as well as Location Services. If everything looked good, a connect call was made on the GoogleClientApi client. If the user was able to correct anything, a suitable intent was fired to launch an activity to take care of it. And if the situation was hopeless, the activity ended. Now let’s look at the various callbacks that could result from these actions.

If the connection request was successful, the onConnected() callback will fire. Listing 19-11 shows what this looks like.

Listing 19-11. Client Is Connected So Request Location Updates

@Override
public void onConnected(Bundle arg0) {
    // Set up location updates
    Log.v(TAG, "Connected!");
    lastFix = FIX.NO_FAIL;
    locator.requestLocationUpdates(client, locReq, this);
    Log.v(TAG, "Requesting location updates (onConnected)...");
}

This one is pretty straightforward. If we got a good connection to Google Play Services, start asking the FusedLocationProviderApi (locator) for location updates. You’ll see more about locReq later, but for now just know that it is a LocationRequest object with parameters that define what kinds of location updates your application wants. This method also resets a state variable (lastFix) which will make more sense soon.

If the connection request was not successful, the onConnectionFailed() callback will fire. Listing 19-12 shows this callback.

Listing 19-12. Handling a Failed Connection Attempt

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
    /*
     * Google Play services can resolve some errors it detects.
     * If the error has a resolution, try sending an Intent to
     * start a Google Play services activity that can resolve
     * the error.
     */
    if (connectionResult.hasResolution()) {
        Log.i(TAG, "Connection failed, trying to resolve it...");
        if(lastFix == FIX.CONNECTION) {
            // Since we're coming through again, it means
            // recovery didn't happen. Time to bail out.
            Log.e(TAG, "Connection retry didn't work");
            finish();
        }
        try {
            // Start an activity that tries to resolve the error
            lastFix = FIX.CONNECTION;
            connectionResult.startResolutionForResult(
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
        } catch (IntentSender.SendIntentException e) {
            // Log the error
            Log.e(TAG, "Could not resolve connection failure");
            e.printStackTrace();
            finish();
        }
    } else {
        /*
         * If no resolution is available, display error to the
         * user.
         */
        Log.e(TAG, "Connection failed, no resolutions available, "+
                GooglePlayServicesUtil.getErrorString(
                        connectionResult.getErrorCode() ));
        Toast.makeText(this, "Connection failed. Cannot continue",
                Toast.LENGTH_LONG).show();
        finish();
    }
}

If the connection request has failed, it is still possible that the situation can be corrected. Once again there is a method that can tell if there is a way to resolve the problem. The ConnectionResult object contains both an indicator if there is a resolution, as well as the intent to fire to try to resolve the situation. In this case, the application calls startResolutionForResult(). Similar to before, an intent will be fired, some activity will be launched, and your application will get a result back in onActivityResult(). Notice that here the request tag is CONNECTION_FAILURE_RESOLUTION_REQUEST. If nothing can be done, display an error and bail out.

There could have been several intents launched, each of which should cause your onActivityResult() callback to fire. Listing 19-13 shows what this callback looks like. Remember that there could have been three separate intents fired to handle problems, so this callback must expect any of the three. Also keep in mind that the intents caused an activity to run, meaning your activity got paused, and it will resume right after the onActivityResult() has fired. This is a major reason why the tryToConnect() method (shown in Listing 19-10) is only called from the activity’s onResume() callback. Whenever this activity is being resumed, it tries to make a new connection to Google Play Services and to set up location updates. When this activity pauses, it disconnects from Google Play Services. It is easy to reconnect rather than trying to hang on to a connection while it is not needed.

Listing 19-13. Getting News Back from the Launched Intents

@Override
protected void onActivityResult(
        int requestCode, int resultCode, Intent data) {
    /* Decide what to do based on the original request code.
     * Note that our activity got paused to launch the other
     * activity, so after this callback runs, our activity's
     * onResume() will run.
     */
    switch (requestCode) {
    case PLAY_SERVICES_RECOVERY_REQUEST :
        Log.v(TAG, "Got a result for Play Services Recovery");
        break;
    case LOCATION_SETTINGS_REQUEST :
        Log.v(TAG, "Got a result for Location Settings");
        break;
    case CONNECTION_FAILURE_RESOLUTION_REQUEST :
        Log.v(TAG, "Got a result for connection failure");
        break;
    }
    Log.v(TAG, "resultCode was " + resultCode);
    Log.v(TAG, "End of onActivityResult");
}

Since onActivityResult() could be called because of a number of intents, the switch statement is used to figure out which one is being responded to. The Google Play Services corrective action might say it was successful by setting the resultCode to Activity.RESULT_OK. This doesn’t necessarily mean that the user fixed the problem, but it tells you that nothing failed. If the response to the Google Play Services corrective action is Activity.RESULT_CANCELED, it could mean there was some sort of failure. Regardless if the user fixed the problem or not, you’re going to return from this callback, and then onResume() will run, in which tryToConnect() will be called again. So it really doesn’t matter what resultCode is. In practice, even when a setting has been properly set for location updates to occur, you could see resultCode set to RESULT_CANCELED. Similarly, if there’s a response to the other fixes, log it and continue since onResume() will run next anyway.

Finally, refer back to the onConnected() callback in Listing 19-11, which calls locator.requestLocationUpdates(client, locReq, this). This is where the FusedLocationProviderApi will be asked to send location updates back to this activity. Google Play Services is up and running, and Location Services are set appropriately.

Once location updates have been requested, any new location updates will get sent to the onLocationChanged() callback. In this sample application, all that happens is that the location information is displayed in a Toast message. The next section goes into more detail on how to request location updates.

There are a few other methods in the activity that so far were not described. The onPause() callback disconnects the client after stopping the location updates. You should notice that the client is checked for connectedness before calling methods. The GoogleApiClient class has a method called isConnected(), which you will use to be sure you request or remove location updates only when there’s a connected client. Otherwise, you will get an IllegalStateException. The two methods for setting up the menu are basic menu callbacks. The menu is used to allow the user to switch between the various priority values. When the user selects a menu item, the location request object is updated and passed back in to alter the location update process. The onCancel() callback can be called from the pop-up error dialog that is shown in tryToConnect (see Listing 19-10). If the user simply closes the error fragment dialog box, we infer that the user doesn’t want to get updates and the application exits.

Location Updates with FusedLocationProviderApi

With the LocationManager, you had to deal with the specific location providers (i.e., GPS or cell/WiFi). With the FusedLocationProviderApi, you submit a LocationRequest and the API will make choices for you of which provider would be the best, not only initially but over time as well. In general, the trade-off when getting location updates is between power consumption and accuracy. GPS is usually more accurate but uses the most power. On the other hand, when indoors, GPS may be less accurate than cell/WiFi, and you’d want to automatically switch to be more accurate while consuming the least amount of power. The FusedLocationProviderApi could also take advantage of on-board sensors such as a gyroscope or compass. This API hides the complexities of location fixing from you.

You should write your code so you’re requesting location updates only when it makes sense to do so. If you are displaying the current location on a map, and the map is not visible, you do not need to request updates. There are cases when you might want to keep getting updates even when not displaying the current position, and we’ll cover that in the next section. The point is that location updates can be a big drain on the battery, so ask for them only when you really need them. You should not assume that the user is going to “be right back” and therefore keep getting updates. If they set their device down and won’t be looking at it again for some time, you’d better not be draining the battery down.

Listing 19-14 shows how the sample application sets the LocationRequest object to make a location updates request of the FusedLocationProviderApi. This is done in the onCreate() callback of the activity.

Listing 19-14. Setting Up a LocationRequest Object

locReq = LocationRequest.create()
        .setPriority(
            LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
        .setInterval(10000)
        .setFastestInterval(5000);

Use the static create() method, then call the appropriate setters to fill out the request object. This object will be passed to the requestLocationUpdates() method of the FusedLocationProviderApi. A big difference from dealing with the older location providers is that this request object does not make any reference to a specific location provider. Similar to the Criteria method of finding a provider, this request object ultimately selects the frequency of updates and the consumption of power.

You can specify the desired frequency of location updates using setInterval() and setFastestInterval(); both take a long argument representing the number of milliseconds. The former is saying that you want to get a location update on a regular basis, every so many milliseconds apart. The system will try to honor this if it is able to, but there are no guarantees. You could get updates more frequently than desired, even much more frequently. That is where the second method comes in. You can specify the fastest interval for receiving location updates. More on this in a bit.

The power portion of the request is handled by the setPriority() setter. There are currently four options for the argument:

  • PRIORITY_NO_POWER
  • PRIORITY_LOW_POWER
  • PRIORITY_BALANCED_POWER_ACCURACY
  • PRIORITY_HIGH_ACCURACY

The NO_POWER option is pretty much saying that your application will be using the passive provider described earlier. The only way to not consume any power is to piggyback off of the location updates for another application. Therefore, the accuracy of the locations may not be very accurate or frequent; it all depends on what other applications are requesting. You just learned that you can request a frequency of updates using setInterval() and setFastestInterval(). If you are piggybacking off of another application, and that application is receiving location updates every 5 seconds, but you don’t want updates faster than every 20 seconds, you should use setFastestInterval(20000) so your application is not overwhelmed with updates. At the same time you could use setInterval(60000) to request a desired interval of one update every minute. If there are few other location updates happening on the device, you won’t have to worry about reducing the frequency from 5 seconds to 20 seconds apart, but at the same time you probably won’t get updates every minute either. You need to use both of these setters to indicate what your application wants, but that doesn’t mean you are guaranteed to get what you want.

The LOW_POWER priority in general means that location updates will be derived only via cell tower triangulation and WiFi hotspot location information. These are low-power ways of determining position, with a corresponding reduction in accuracy. You could easily find the locations to be accurate only to within 1,500 meters or worse, but then you could get a location that’s accurate to 10 meters. All of the priorities will take advantage of the passive provider, so if an accurate location update happens to be requested by some other application, your application could pick it up even when your priority is set to low power.

The BALANCED priority will try to do a decent job of trading off accuracy for less power. It will consider using all of the available methods of determining location, except for GPS.

The HIGH_ACCURACY priority will potentially use all available sources of location information, including GPS. Because of the GPS radio, this priority could consume a lot of battery.

Location updates also depend on the location mode of the device. As you saw earlier, the Location Settings changed in KitKat to allow the user to specify a mode of location updates for their device. Referring now to the Settings.Secure class, the location mode setting values are as follows:

  • LOCATION_MODE_OFF
  • LOCATION_MODE_BATTERY_SAVING
  • LOCATION_MODE_HIGH_ACCURACY
  • LOCATION_MODE_SENSORS_ONLY

and the current value can be retrieved using the code from Listing 19-8. The mode is set by the user for the entire device, not by application. However, your application has an opportunity to request a priority to complement the mode choice made by the user. If the device has a mode of HIGH_ACCURACY and your application chooses a priority of LOW_POWER, your application will not be the one draining the battery but could still get decent location updates.

The mode can work against you however. If the user chooses a mode of SENSORS_ONLY, and the priority is set to NO_POWER, LOW_POWER or even BALANCED, location updates will be rare, regardless of what you set in the location request with setInterval(). The preferred mode for most useful location updates is HIGH_ACCURACY, because this mode will combine all possible sources of location information and provide the most accurate results. Your application will be able to get high accuracy when needed (hopefully this is a rare need) and good accuracy the rest of the time. Your application can alter the priority to HIGH_ACCURACY when needed, but BALANCED or LOW_POWER the other times.

Some other interesting options with a LocationRequest include setting a specific number of location updates to receive, or to specify a time limit when the location updates should stop. You can also set a minimum distance (in meters) within which your application does not want updates. This is a geofence of sorts, where you tell the location service that you only want a location update if the device moves a certain distance from its current location. That is in effect setting up a geofence circle around the current location. More on geofences later.

Alternate Ways of Getting Location Updates

You’ve seen how to get location updates sent to your activity using the requestLocationUpdates() method of the LocationManager and the FusedLocationProviderApi. There are actually several different signatures of this method, including ones that use a PendingIntent. This gives you the ability to direct location updates to services or broadcast receivers. You can also direct location updates to other Looper threads instead of the main thread, giving you lots of flexibility for your application, although some of these methods have been available only since Android 2.3.

Using Proximity Alerts and Geofencing

Geofencing is a popular requirement for a mobile application. It means that your application should alter its behavior depending on where it is located. A typical use case is to prevent the device from working when it is outside of a particular location. For example, a hospital application could restrict access to patient data when it is not at the hospital. Or your application might want to silence notifications when the device is at the workplace. LocationManager has a mechanism called proximity alerts, and there is a similar recent API called GeofencingApi for the newer Location Services. We’ll briefly discuss the first, then address the second in detail.

We mentioned earlier that the LocationManager can notify you when the device enters a specified geographical location. The method to set this up is addProximityAlert() from the LocationManager class. Basically, you tell the LocationManager that you want an Intent to be fired when the location of the device goes into, or leaves, a circle of a certain radius with a center at a latitude/longitude position. The Intent can trigger a BroadcastReceiver or a Service to be called, or an Activity to be started. There is also an optional time limit placed on the alert, so it could time out before the Intent fires.

Internally, the code for this method registers listeners for both the GPS and network providers and sets up location updates for once per second and a minDistance of 1 meter. You don’t have any way to override this behavior or set parameters. Therefore, if you leave this running for a long time, you could end up draining the battery very quickly. If the screen goes to sleep, proximity alerts will only be checked once every four minutes, but again, you have no control over the time duration here. For these reasons, we have included a demonstration application called ProximityAlertDemo with the sample applications, but we will not dive into the details. Instead, we will turn our attention to the Location Services approach, with another sample application called GeofencingApi. Note that the GeofencingApi sample application will look similar to the FusedLocationProviderApi sample application since both share the GoogleClientApi mechanism for activation.

The GeofencingApi API

At the time of this writing, a geofence is a circular region with a latitude/longitude center, plus some time parameters. At some point in the future, the region might not be circular but for now it is. Once a geofence has been built, it can be passed to the GeofenceApi for monitoring. Your application can even go away and your geofence can be active. Along with a geofence, or set of geofences, your application will pass a PendingIntent with an Intent to be fired when something interesting happens around a geofence. The three current events are enter, exit and dwell. Enter and exit are simple to understand; the Intent will be fired if the device goes into, or leaves, the circular region. The dwell event fires the Intent after the device remains inside of the circular region for a period of time. This loitering delay is specified in milliseconds. And that’s all there is to it.

See the sample application called GeofencingApiDemo. It sets up two geofences called home and work, connects to Location Services, and registers a service intent to be fired when the device enters, exits or dwells in either of these geofences. When triggered, the service generates a notification per event to make it easier for you to see the results. Geofences are often used in the background, so a service makes a lot of sense here. That is, an application shouldn’t need to be in the foreground to have geofences. In fact, the basic idea of a geofence is that you want your application to be wakened up if the device enters or leaves a specific geographic region.

The setup code used earlier to make sure that Google Play Services and Location Services are available and ready has been left out of this sample application to make it easier to follow along, but you would want to include that code in a production application. Listing 19-15 shows the onCreate() method of the main activity, in which the geofences and the PendingIntent are created.

Listing 19-15. Setting Up Geofences

private GoogleApiClient mClient = null;
private List<Geofence> mGeofences = new ArrayList<Geofence>();
private PendingIntent pIntent = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final float radius = 0.5f * 1609.0f; // half mile times 1609 meters per mile

    Geofence.Builder gb = new Geofence.Builder();
    // Make a half mile geofence around your home
    Geofence home = gb.setCircularRegion(28.993818, -81.383816, radius)
            .setTransitionTypes(
                Geofence.GEOFENCE_TRANSITION_ENTER |
                Geofence.GEOFENCE_TRANSITION_EXIT |
                Geofence.GEOFENCE_TRANSITION_DWELL )
            .setExpirationDuration(
                12 * 60 * 60 * 1000)  // 12 hours
            .setLoiteringDelay(300000)   // 5 minutes
            .setRequestId("home")
            .setNotificationResponsiveness(5000) // 5 secs
            .build();
    mGeofences.add(home);

    // Make another geofence around your work
    Geofence work = gb.setCircularRegion(28.36631, -81.52120, radius)
            .setRequestId("work")
            .build();
    mGeofences.add(work);
    Intent intent = new Intent(this, ReceiveTransitionsIntentService.class);

    pIntent = PendingIntent.getService(getApplicationContext(), 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT);

    mClient = new GoogleApiClient.Builder(this, this, this)
            .addApi(LocationServices.API)
            .build();
    
    Log.v(TAG, "Activity, client are created");
}

See how the geofence is created as a circle around a lat/lon, with the events of interest (in this case all of them) and some time parameters. In this sample, the geofences will be active for 12 hours, or until they are removed (see onDestroy()). It’s also possible to set geofences to never expire. The loitering delay of 5 minutes means that the dwell event will fire if the device stays inside the geofence for at least 5 minutes. The request ID will be passed back to your application with the Intent so you can identify which geofence the Intent is for. The notification responsiveness of 5 seconds means that the GeofencingApi will try to send the Intent within 5 seconds of when the event happens. However, there are no guarantees that the Intent will be that quick. The larger this value, the better it is on battery life, since the API could sleep more and check things less often. On the other hand, if this value is very long, for example several minutes, it is possible you might even miss an event if the device passes through your geofence quickly. The choice of notification responsiveness will depend on how big your geofences are and how you want your application to behave.

Similar to the previous sample application, a connection is attempted from onResume(), and Listing 19-16 shows what runs when the connection is successful.

Listing 19-16. Registering Geofences with the API

@Override
public void onConnected(Bundle arg0) {
    // Set up geofences
    Log.v(TAG, "Setting up geofences (onConnected)...");
    PendingResult<Status> pResult = mFencer.addGeofences(mClient,
            mGeofences, pIntent);
    pResult.setResultCallback(this);  // ResultCallback<Status> interface
}

@Override
public void onResult(Status status) {
    Log.v(TAG, "Got a result from addGeofences("
        + status.getStatusCode() + "): "
        + status.getStatus().getStatusMessage());
}

The GeofencingApi gets passed the API client handle, the list of geofences, and the PendingIntent. The return is a PendingResult. If you want to find out if the result is ultimately successful or not, you need to set a callback receiver using setResultCallback(). This activity has implemented the ResultCallback<Status> interface, so the onResult() callback will be invoked with the results of the addGeofences() method call. For this sample, the result is simply logged, but of course you would want to take steps if the result was not successful. That’s all that the activity does. Next up is the service that receives an Intent when an interesting event occurs.

Listing 19-17 shows the interesting callbacks and methods of the ReceiveTransitionsIntentService, an IntentService for this application. It basically reports out the information received, whether that is an error or a geofence event. Events are displayed using notifications. This is for your safety since the expectation is that you will start this application at home and drive to work. We do not want you having to watch the device’s screen during the trip. Instead, you will be able to review all of the notifications from each event when you are safely stopped.

Listing 19-17. Receiving Intents from the GeofencingApi

public void onCreate() {
    super.onCreate();
    notificationMgr = (NotificationManager)getSystemService(
            NOTIFICATION_SERVICE);
}

@Override
protected void onHandleIntent(Intent intent) {
    GeofencingEvent gfEvent = GeofencingEvent.fromIntent(intent);
    // First check for errors
    if (gfEvent.hasError()) {
        // Get the error code with a static method
        int errorCode = gfEvent.getErrorCode();
        // Log the error
        Log.e(TAG, "Location Services error: " +
                Integer.toString(errorCode));
    /*
     * If there's no error, get the transition type and the IDs
     * of the geofence or geofences that triggered the transition
     */
    } else {
        // Get the type of transition (entry or exit)
        int transitionType =
                gfEvent.getGeofenceTransition();
        String tranTypeStr = "UNKNOWN(" + transitionType + ")";
        switch(transitionType) {
        case Geofence.GEOFENCE_TRANSITION_ENTER:
            tranTypeStr = "ENTER";
            break;
        case Geofence.GEOFENCE_TRANSITION_EXIT:
            tranTypeStr = "EXIT";
            break;
        case Geofence.GEOFENCE_TRANSITION_DWELL:
            tranTypeStr = "DWELL";
            break;
        }
        Log.v(TAG, "transitionType reported: " + tranTypeStr);
        Location triggerLoc = gfEvent.getTriggeringLocation();
        Log.v(TAG, "triggering location is " + triggerLoc);

        List <Geofence> triggerList =
                   gfEvent.getTriggeringGeofences();

        String[] triggerIds = new String[triggerList.size()];

        for (int i = 0; i < triggerIds.length; i++) {
            // Grab the Id of each geofence
            triggerIds[i] = triggerList.get(i).getRequestId();
            String msg = tranTypeStr + ": " + triggerLoc.getLatitude() +
                ", " + triggerLoc.getLongitude();
            String title = triggerIds[i];
            displayNotificationMessage(title, msg);
        }
    }
}

private void displayNotificationMessage(String title, String message)
{
    int notif_id = (int) (System.currentTimeMillis() & 0xFFL);

    Notification notification = new NotificationCompat.Builder(this)
    .setContentTitle(title)
    .setContentText(message)
    .setSmallIcon(android.R.drawable.ic_menu_compass)
    .setOngoing(false)
    .build();

    notificationMgr.notify(notif_id, notification);
}

When you replace the latitude and longitude of home and work in this application, you run it on a real device, and you then move the device, you will see notifications such as those in Figure 19-8.

9781430246800_Fig19-08.jpg

Figure 19-8. Notifications from GeofencingApi events

The first event occurred at 6:40 pm and happened because the device was already inside the home region when the app was started. The second event at 6:45 pm is a dwell event because the device is still within the home region after the loitering delay of 5 minutes. Had the device left the home region before the screenshot was captured, there would have been an exit event from home. Note that the latitude and longitude in the notification are the actual location of the device and not necessarily the center of the region.

References

Here are helpful references you may wish to explore further.

Summary

Let’s conclude this chapter by quickly enumerating what you have learned about maps so far:

  • How to get your own Maps API key from Google.
  • MapFragment, the main component for all maps.
  • The modifications you need to make to your AndroidManifest.xml file to get a maps application to work.
  • Defining a layout to contain a map, and how to instantiate a map.
  • Zooming in and out, panning and showing the current location.
  • Including different modes such as satellite and traffic.
  • How map tiles are used to render maps.
  • Adding markers to your maps.
  • Map cameras and methods to set a zoom level that accommodates a specific set of markers.
  • The Geocoder, and how it converts from address to latitude/longitude, or from latitude/longitude to addresses and places of interest.
  • Putting the Geocoder into a background thread to avoid nasty Application Not Responding (ANR) pop-ups.
  • The LocationServices service, which uses GPS and/or network towers to pinpoint the location of the device.
  • Selecting a location provider, and what to do if the desired location service or provider is not enabled.
  • Using the emulator’s features to send location events to your application for testing. This includes using special files that record entire series of location events.
  • Using methods of the Location class to, for example, calculate distances between points.
  • How to do all of the checks and corrective actions to set up Google Play Services for Location Updates.
  • Alerting on proximity—that is, setting up a proximity and being alerted when the device enters or leaves that proximity.
  • Setting up geofences to act on enter, exit, and dwell events for one or more regions while conserving battery life.
..................Content has been hidden....................

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