Previous  | Next  | Home

Mapping Demo


 

Let us now construct a somewhat more involved project that will show how to use the Google mapping libraries to add new functionalities to that possible with the Android API alone. In this project we shall

Because this will be a little more complex than our first examples, we outline it first according to the prescription suggested in Creating an Application.

 

Outlining the Project

First we sketch our goals. We wish to implement a project that will do the following:

  1. Demonstrate how to accept a user-input location and open Google maps on that location (place name or latitude/longitude for input).

  2. Implement in the resulting map view a toggle between satellite view and map view, and a toggle to turn traffic view on and off.

  3. Add capability to send the device to the present GPS location and display in Google maps, overlay a point indicating the present position and a compass indicating direction, and track the location continuously re-centering the map if the device moves.

  4. Add to the resulting map view continuously-updated information about the latitude/longitude, number of GPS satellites being tracked, the precision of the fix, and so on. Add a way to toggle this on and off.

  5. Add an options popup menu with Settings, Help, and Quit options. In the settings options, allow the user to change parameters controlling the accuracy versus power consumption of the GPS fix, and to toggle the compass view on and off.

In the figure below we sketch roughly the layout of how we propose to organize the main screen and secondary screens to accomplish this, assign proposed names for the required class and XML files, list special permissions and keys we are going to need, and list some strings that we will need as resources (we will undoubtedly need others, but it is clear that we will need these at least). The connecting arrows between the different screens also suggest qualitatively the event handlers and associated actions that will be required.

We will undoubtedly have to modify this some as we develop the project, but this gives us a clear starting point.

 

Creating the Project

Now that we have sketched the outlines of the project, it is time to construct it. Open Eclipse and click File > New > Android Project.

We are going to choose to target Android 2.1-update1 for this project, which will involve the Google mapping API so we will need the Google APIs for API Level 7, but we will specifiy a minimum SDK level of 3 (corresponding to Android 1.5). Fill out the resulting window as in the following figure.



and click Finish. A project MappingDemo should now appear in the left panel of the Eclipse interface. Open its AndroidManifest.xml file. Since we may test on an actual device, select the Application tab at the bottom and set Debuggable to true (which will automatically insert a debuggable = true attribute in the manifest file). Then select the AndroidManifest.xml tab at the bottom to display the file. Add to the file lines that give permission for internet access, coarse-grain and fine-grain location information, and access to the Google maps library:



    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.lightcone.mappingdemo"
        android:versionCode="1"
        android:versionName="1.0">
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        <application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="true">
            <activity android:name=".MappingDemo"
                    android:label="@string/app_name">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <uses-library android:name="com.google.android.maps" />
        </application>
        <uses-sdk android:minSdkVersion="3" />
    </manifest> 

Save the AndroidManifest.xml file. Open the strings.xml file under res/values and edit it to define the strings


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string name="hello">Mapping Demo</string>
        <string name="app_name">Mapping Demo</string>
        <string name="locLabel">Location:</string>
        <string name="goLabel">Go</string>
        <string name="latLabel">Lat</string>
        <string name="longLabel">Long</string>
        <string name="trackLabel">Track Present Location</string>
        <string name="satLabel">Sat</string>
        <string name="mapLabel">Map</string>
    </resources>

Save the file strings.xml.

In Eclipse, choose Project > Clean, select the MappingDemo project and click OK. This will do a clean build of the project. There should be no errors at this point (no red xs showing on any files in the project). If there are red xs, hover the mouse over the xs and the things underlined in wavy red to see what the errors are and how to fix them, and fix them (see the example of using Eclipse systematically to correct errors in WebView Demo).

 

The Opening Screen

We are now ready to lay out the opening screen of our app, and then to tie that to the class defined by MappingDemo.java. Open res/layout and double-click on main.xml to open it in the editor. From the diagram outlining our project above we see that on the opening screen we need to lay out three editable text fields, corresponding to the Android class EditText and three buttons, corresponding to the Android class Button. (The Options menu popup opens when MENU is selected on the device and will be dealt with later). The layout sketched in our outline can be achieved if we edit the main.xml file to read


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:padding="10dip">
    
        <LinearLayout 
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="horizontal">
            
            <EditText
                android:id="@+id/geocode_input"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1.0"
                android:hint="(Place name)"
                android:lines="1"
                android:inputType="text" />
        
            <Button 
                android:id="@+id/geocode_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/goLabel" />
                        
        </LinearLayout>
                
        <LinearLayout 
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:padding="3sp"
            android:orientation="horizontal">
                
            <EditText
                android:id="@+id/lat_input"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1.0"
                android:hint="(Lat deg)"
                android:lines="1"
                android:inputType="numberDecimal|numberSigned" />
        
            <EditText
                android:id="@+id/lon_input"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1.0"
                android:hint="(Lon deg)"
                android:lines="1"
                android:inputType="numberDecimal|numberSigned" />
        
            <Button 
                android:id="@+id/latlong_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/goLabel" />
                        
        </LinearLayout>
                
            <Button 
                android:id="@+id/presentLocation_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="@string/trackLabel" />
                    
    </LinearLayout>

The documentation explaining the components of this layout may be found at LinearLayout, EditText, and Button, but briefly

Thus, reading vertically, main.xml should lay out a row containing a textfield and a button arrayed across the entire screen, then a row with two textfields and a button arrayed across the entire screen, and finally a row with one button centered on the screen.

Let's check our progress. Execute MappingDemo on an emulator or a phone (which must implement the Google maps API if it is an emulator). You should see something like the following image



which is from a Motorola Backflip phone running Android 1.5.

 

Restrictions on TextFields

Notice that the XML attributes used in laying out this page have given certain capabilities and placed certain restrictions on text fields automatically. The first text field is set up to accept general text, but the other two text fields will accept only decimal numbers, possibly with a negative sign.

Try it and see; you should find that you can type anything into the first field, but only digits, a single period, and a minus sign (only if it is the first entry) in the other two fields. We have also indicated with the android:hint attributes further guidance for the user. For example, android:hint="(Lat deg)" inserts initially in the corresponding field a hint that the user is to enter the latitude in degrees (this hint will disappear as soon as the user begins to type into the field). When the hints are displayed in the text fields, the actual content of the field is the empty string, which is the reason for the logic statements in the event handlers defined later that do not launch the intents if the fields have not been filled in.


If you execute on a device it is possible that autocompletion will be enabled on the first text field. For example, on the Motorola Backflip sold by AT&T, if you type in the string "nair" you should get something like the following figure,



which illustrates text completion in the first EditText field. For example, if we were entering Nairobi, we could select it off the bar below the text field at this point, without typing further.

Automatic text completion in an EditText field can be implemented using the class AutoCompleteTextView, which extends EditText. It requires that the programmer supply the list of words against which the autocompletion will be checked. It appears that in this case on the Backflip phone, autocompletion has been enabled for EditText fields against a rudimentary dictionary (the dictionary does not appear to be too extensive, because some common words don't trigger autocompletion---for example "triangle" and "square" are in its word list, but not "pentagon").

 

Adding Listeners

We have the initial screen laid out as we desired, so now we need to attach some actions to its widgets. Open MappingDemo.java. To listen for button clicks on the opening screen we add "implements OnClickListener" to the class definition, which necessitates importing several Android classes (if not already imported) and adding an onClickView(View v) method stub, as described in WebView Demo. With the resulting changes MappingDemo.java now reads


    package com.lightcone.mappingdemo;
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    
    public class MappingDemo extends Activity implements OnClickListener {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
        }
    
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub	
        }
    }

Now we fill out the onClick method to deal with click events on the main screen. Consulting main.xml we see that the three buttons have id's geocode_button, latlong_button, and presentLocation_button, respectively. When these buttons are pressed we wish to launch new screens, which we will accomplish with intents, and we will define the contents of the new screens with two new classes: ShowTheMap (when either of the first two buttons is pushed) and MapMe (when the third button is pushed).

Let's first define the stubs for the new classes that we will need (with the details to be filled in below). Create the class ShowTheMap:

  1. In Eclipse, right-click the package name under MappingDemo (e.g., com.lightcone.mappingdemo).

  2. Select New > Class on the resulting screen.

  3. Give the class the name ShowTheMap and select the Superclass com.google.android.maps.MapActivity. Leave everything else at the default values.

  4. Click OK.

In the same manner, create the class MapMe.

New files ShowTheMap.java and MapMe.java will now appear in the src/<packagename> directory. Check them to be sure that they extend MapActivity, and that they contain a stub for the method isRouteDisplayed(), which the user is required to implement because in the class MapActivity that we are extending this method is abstract (they should if the correct choices were made above). We'll edit these new classes later.

Now we modify the class MappingDemo by adding click listeners for all buttons and adding a switch statement in the onClick method to sort out which button has been pushed (which requires two new imports). The class listing with changes in red is:


    package com.lightcone.mappingdemo;

    import android.app.Activity;
    import android.os.Bundle;
    import android.content.Intent;
    import android.util.Log;
    import android.view.View;
    import android.view.View.OnClickListener;

    public class MappingDemo extends Activity implements OnClickListener {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            
            // Add Click listeners for all buttons
            View firstButton = findViewById(R.id.geocode_button);
            firstButton.setOnClickListener(this);
            View secondButton = findViewById(R.id.latlong_button);
            secondButton.setOnClickListener(this);
            View thirdButton = findViewById(R.id.presentLocation_button);
            thirdButton.setOnClickListener(this);
	    
        }
    
        @Override
        public void onClick(View v) {

            switch(v.getId()){
                case R.id.geocode_button:
                    Log.i("Button","Button 1 pushed");
                    Intent j = new Intent(this, ShowTheMap.class);
                    startActivity(j);
                break;
                
                case R.id.latlong_button:
                    Log.i("Button","Button 2 pushed");
                    Intent k = new Intent(this, ShowTheMap.class);
                    startActivity(k);
                break;
                
                case R.id.presentLocation_button:
                    Log.i("Button","Button 3 pushed");
                    Intent m = new Intent(this, MapMe.class);
                    startActivity(m);
                break;	
            }	

        }
    }

where we have inserted some diagnostic logcat statements to allow us to check the logic of the event handling. Let's test it. Launch an emulator or target a device and click one of the Go buttons. Uh-oh; the app dies with an error message requiring a forced close. Checking the logcat output, we see the reason: the logcat output contains the error message


06-14 10:47:10.696 E/AndroidRuntime( 3000): android.content.ActivityNotFoundException: Unable to find explicit activity
class {com.lightcone.mappingdemo/com.lightcone.mappingdemo.ShowTheMap}; have you declared this activity in your
AndroidManifest.xml?

Android requires that all activities be registered in the AndroidManifest.xml file. Since we forgot to do that for the new activities that we added corresponding to the classes ShowTheMap and MapMe, the Android runtime is unable to find those classes. We fix that by adding to AndroidManifest.xml within the application tag declarations for the new activities such that the application tag now reads


    <application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="true">
        <activity android:name=".MappingDemo"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ShowTheMap" android:label="Lat/Long Location"> </activity>
        <activity android:name=".MapMe" android:label="Track Present Location"> </activity>
        <uses-library android:name="com.google.android.maps" />
    </application>

which registers the new activities ShowTheMap and MapMe. Now if we launch the app on an emulator or phone and push the buttons we get the expected results:

  1. If we click the top button a blank screen with the title Lat/Long Location appears (the title is specified by the android:label attribute in the activity tag of the manifest) and in the logcat output we see our diagnostic message "Button 1 pushed".

  2. If we click the Return button (the curved back arrow on the phone or emulator) to return to the main screen and click the second button the same new screen appears as before, but now "Button 2 pushed" appears in the logcat output,

  3. Finally if we return to the main screen and push the bottom button we get a new screen with the title "Track Present Location" and "Button 3 pushed" is output in the logcat stream.

Thus, our buttons and intents appear to be behaving correctly.

 

Implementing a Google Maps Display

Let's now modify the class ShowTheMap so that it can implement a Google maps display centered on a specified latitude and longitude.


The key class in the Maps external library is MapView, which is a subclass of the standard Android class ViewGroup. MapView provides a wrapper around the Google Maps API that lets you manipulate Maps data and control the view just as for any other View. A MapView displays a map with data obtained from the Google Maps service. It can capture keypresses and touch gestures to pan and zoom, handle network requests for maps tiles, and provide all of the UI elements necessary to control the map. An application can manipulate the MapView using the methods of the MapView class, and can draw a number of Overlay types over the displayed map that can respond to keypresses and other interactions.

Right-click on layout under res/layout and choose New > Android XML File to create an XML file of type Layout named showthemap.xml in the folder res/layout, giving it a LinearLayout root element. Then, edit the file showthemap.xml that this produces to add a view tag inside the parent LinearLayout:


<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    
    <view android:id="@+id/mv"
        class="com.google.android.maps.MapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1" 
        android:clickable="true"
        android:apiKey="myMapKey"
        /> 	
    </LinearLayout>

There are three things about this view layout that are a little different from things we have done up to now:

  1. The class corresponding to the view is a fully qualified string, com.google.android.maps.MapView, because MapView is not in the com.google.android.widget namespace.

  2. The attribute clickable has been set to true.

  3. An attribute called an android:apiKey has been inserted.

The first of these is required to implement Google map classes (which, recall, are separate from but compatible with the Android classes), and the second is necessary to allow some later click actions on the maps that we will create. Explanation of the third is slightly more complicated, but essential. Embedding Google maps under API control will not work unless a unique apiKey is specified (for each MapView in the project). Thus, before proceeding we must see how to obtain and use an apiKey for maps.

 

The Google Maps API Key

We are going to use the MapView class to integrate Google Maps into our application. Because MapView gives you access to Google Maps data, you must register with the Google Maps service before your implementation of MapView will be able to obtain map data. The procedure for doing this is described in the Appendix The Maps API Key. Follow the instructions given there to obtain a temporary Maps API key that will be necessary in what follows. Insert the Maps API key that you obtain in the showthemap.xml file, which should now read


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"> 
  <view android:id="@+id/mv"
    class="com.google.android.maps.MapView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1" 
    android:clickable="true"
    android:apiKey="07WVUg-srWUbhpkGj1mi0w56xD9nMEu0NhqJeYg"
    />
</LinearLayout>

except that you should substitute your value of android:apiKey.


This key is machine-specific for your development computer if you obtained a temporary one using the debug certificate. Thus, if you develop on more than one machine, each will have a different apiKey and you will have to change the entry in the showthemap.xml file to the appropriate one if you move the MappingDemo project---or any project using MapView---between machines.

Now open the file ShowTheMap.java and edit it to read


    package com.lightcone.mappingdemo;
    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.MapActivity;
    import com.google.android.maps.MapController;
    import com.google.android.maps.MapView;
    import android.os.Bundle;
    import android.view.Window;
    
    public class ShowTheMap extends MapActivity {
            
        private static double lat = 35.952967;   // Temporary test values for lat/long
        private static double lon = -83.929158 ;
        private int latE6;
        private int lonE6;
        private MapController mapControl;
        private GeoPoint gp;
        private MapView mapView;
            
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);  // Suppress title bar to give more space
            setContentView(R.layout.showthemap);
            
            // Add map controller with zoom controls
            mapView = (MapView) findViewById(R.id.mv);
            mapView.setSatellite(true);
            mapView.setTraffic(false);
            mapView.setBuiltInZoomControls(true);   // Set android:clickable=true in main.xml
            int maxZoom = mapView.getMaxZoomLevel();
            int initZoom = (int)(0.80*(double)maxZoom);
            mapControl = mapView.getController();
            mapControl.setZoom(initZoom);
            // Convert lat/long in degrees into integers in microdegrees
            latE6 =  (int) (lat*1e6);
            lonE6 = (int) (lon*1e6);
            gp = new GeoPoint(latE6, lonE6);
            mapControl.animateTo(gp);    
        }
                
        // Required method since class extends MapActivity
        @Override
        protected boolean isRouteDisplayed() {
            return false;  // Don't display a route
        }
    }

The relevant documentation may be found under MapView, MapActivity, and MapController. A MapView is a View containing a map, ShowTheMap extends MapActivity, which is the class that manages lifecycles and related issues for MapView displays, and MapController supplies methods to manage panning and zooming of a map display. A GeoPoint represents a latitude/longitude pair, stored as an integer number of microdegrees (which may be obtained from the latitude and longitude in degrees by multiplying them by a million).

We have for now hardwired in a latitude and longitude to test things; we'll replace that by user input below. Note that the ability to pan and zoom requires setBuiltInZoomControls(true), and also requires that the clickable attribute in showthepage.xml be set to true. If you test this you should find that pressing the second button sends you to a map display centered on the latitude and longitude values that we hardwired in (the coordinates correspond to the University of Tennessee, Knoxville).

 

Using the Keyboard to Toggle Views

Let's use the onKeyDown method of MapActivity (which it inherits from Activity) and the constants of the KeyEvents class to add the ability to toggle between map view and satellite view, and between traffic and no traffic view, using the device keyboard. Add the following method to ShowTheMap.java


    public boolean onKeyDown(int keyCode, KeyEvent e){
        if(keyCode == KeyEvent.KEYCODE_S){
            mapView.setSatellite(!mapView.isSatellite());
            return true;
        } else if(keyCode == KeyEvent.KEYCODE_T){
            mapView.setTraffic(!mapView.isTraffic());
            mapControl.animateTo(gp);  // To ensure change displays immediately
        }
        return(super.onKeyDown(keyCode, e));
    }


(which will require adding import android.view.KeyEvent). Now when you execute the program and display a map, you should be able to toggle between satellite and map view by pressing the "s" key on the phone, and between traffic and no traffic by pressing the "t" key.


Traffic overlays are often provided only for busy urban routes. To get the soft keyboard on a many Android phones, press and hold (long-press) the MENU key. Note, however, that this may be device dependent. On my Motorola Backflip phone, long-pressing the MENU key brings up the soft keyboard allowing the toggles between map views described above. But on my Captivate (Samsung Galaxy S) phone the MENU key has been co-opted to bring up a soft keyboard attached to a search field when long-pressed, which captures the keypresses and doesn't allow the above keyboard toggles to work. This latter behavior illustrates an important point for user-friendly application development. Since Android can run on many devices that can be customized by their manufacturers, you cannot always assume that particular things like soft keyboards will be accessed the same way on all devices. Thus, the wise developer builds redundancy into an application. In the above example, a simple solution would be to also allow a toggle between map views in an options menu (see an example of building an options menu later in this project).

 

Reading and Interpreting Input in TextFields

Now we wish to enable the input fields to read latitude/longitude and place names, and send the device to the corresponding location. Let's do the latitude/longitude first. We begin by adding to ShowTheMap a static method to insert the latitude and longitude. Open ShowTheMap.java and add inside the class definition the method


    public static void putLatLong(double latitude, double longitude){
        lat = latitude;
        lon = longitude;
    }

Then remove the explicit latitude and longitude values that we hardwired in earlier for testing, changing the corresponding lines in ShowTheMap to read


    private static double lat;
    private static double lon;

Now edit MappingDemo.java so that when the second button is pushed we read the user input in the two EditText input fields and interpret them as latitude and longitude variables. First add a new import and the class variables lat and lon:


    import android.widget.EditText;

    public class MappingDemo extends Activity implements OnClickListener {	
        public double lat;
        public double lon;
        .
        .
        .

and then modify the switch-statement case corresponding to the second button to read


    case R.id.latlong_button:
				
        // Read the latitude and longitude from the input fields
        EditText latText = (EditText) findViewById(R.id.lat_input);
        EditText lonText = (EditText) findViewById(R.id.lon_input);	
        String latString = latText.getText().toString();
        String lonString = lonText.getText().toString();
        // Only execute if user has put entries in both lat and long fields.
        if(latString.compareTo("") != 0 && lonString.compareTo("") != 0){
            lat = Double.parseDouble(latString);
            lon = Double.parseDouble(lonString);          
            Intent k = new Intent(this, ShowTheMap.class);
            ShowTheMap.putLatLong(lat,lon);
            startActivity(k);
        }

        break;

where the if-statement is to ensure that actions are performed only if the user has put entries in both the latitude and longitude fields. The basic procedure is to

  1. extract the contents of the fields as EditText objects,

  2. use EditText methods getText() and toString() to convert this input to strings, and then

  3. use the parseDouble method of the Java class Double to convert the strings to doubles.

Once we have stored the latitude and longitude in the variables lat and lon, we then use an intent to launch the ShowTheMap activity to display the map, and use the putLatLong(lat,lon) method of ShowTheMap to pass the coordinates input by the user to use in centering the map display.

 

Sending the Map to Specific Latitudes and Longitudes

Now if you insert latitude and longitude values in degrees and push the corresponding button, the map display screen should center on the location corresponding to the input latitude and longitude. For example, try

In all these cases the satellite view (toggled with "s" on the device keyboard) is the most interesting, and you may have to change the map zoom for the best view (touching the screen should give you zoom controls; dragging should give you pan control).


As an aside, here is one way to get precise longitude and latitude coordinates for arbitrary locations: Using Google Maps on your computer, navigate to the desired area, right-click on the desired point and select Center map here, click Link at top of map, left-click on Paste link in email or IM field, right-click and select Copy, and past the copy into a text editor. In the html address, the field of the form

ll=35.832554,-84.060522

contains the latitude (first number) and longitude (second number) in degrees. Multiply these by 1e6 (shift decimal 6 places right) to get microdegrees for the maps API arguments.

 

Geocoding

Now let's make it even simpler for the user to send this mapping application to a location of interest. It would be much easier to just type in "Hoover Dam", or "1516 Android Way", or "Big Ben", rather than trying to figure out the latitude and longitude coordinates for locations. The Android/Google API in fact has classes that allow us to implement that capability with just a few lines of code. The most important resource is the Android Geocoder class, which is a class for handling geocoding and reverse geocoding:

In the file MappingDemo.java, first add the imports


    import java.io.IOException;
    import java.util.Iterator;
    import java.util.List;
    import android.location.Address;
    import android.location.Geocoder;

and then modify the case-clause corresponding to the first button to read


    case R.id.geocode_button:
					
        // Following adapted from Conder and Darcey, pp.321 ff.		
        EditText placeText = (EditText) findViewById(R.id.geocode_input);			
        String placeName = placeText.getText().toString();
        // Break from execution if the user has not entered anything in the field
        if(placeName.compareTo("")==0) break;
        int numberOptions = 5;
        String [] optionArray = new String[numberOptions];
        Geocoder gcoder = new Geocoder(this);  
          
        // Note that the Geocoder uses synchronous network access, so in a serious application
        // it would be best to put it on a background thread to prevent blocking the main UI if network
        // access is slow. Here we are just giving an example of how to use it so, for simplicity, we
        // don't put it on a separate thread.
                            
        try{
            List<Address> results = gcoder.getFromLocationName(placeName,numberOptions);
            Iterator<Address> locations = results.iterator();
            String raw = "\nRaw String:\n";
            String country;
            int opCount = 0;
            while(locations.hasNext()){
                Address location = locations.next();
                lat = location.getLatitude();
                lon = location.getLongitude();
                country = location.getCountryName();
                if(country == null) {
                    country = "";
                } else {
                    country =  ", "+country;
                }
                raw += location+"\n";
                optionArray[opCount] = location.getAddressLine(0)+", "+location.getAddressLine(1)+country+"\n";
                opCount ++;
            }
            Log.i("Location-List", raw);
            Log.i("Location-List","\nOptions:\n");
            for(int i=0; i<opCount; i++){
                Log.i("Location-List","("+(i+1)+") "+optionArray[i]);
            }
					
        } catch (IOException e){
            Log.e("Geocoder", "I/O Failure; is network available?",e);
        }			
        Intent j = new Intent(this, ShowTheMap.class);
        ShowTheMap.putLatLong(lat,lon);
        startActivity(j);

        break;


As noted in the code comments above, it may be useful to call getFromLocationName from a thread separate from your primary UI thread (see the Geocoder documentation). The getFromLocationName method throws both IllegalArgumentException (for a null place name) and IOException (if there is an i/o problem like the network not being available). These could be used to make a more user-friendly application (we are already catching the IOException, but not using it except for diagnostic output). Address is an Android class that represents a location address as a set of strings.

Try entering the locations "hoover dam" or "little mermaid" or "ord" (the airport identifier for Chicago O'Hare Airport) and clicking Go. On a Motorola Backflip phone running Android 1.5, the first two options give the following figures.



("Den Lille Havfrue" is the Danish name for the statue known as "The Little Mermaid" in English.) NOTE: This works (compiled under Android 2.1) with a phone or emulator running Android 1.5, but I found that it may fail due to a network error if you attempt to execute it on an emulator running later versions of the operating system.

In the preceding examples, if you check the diagnostics we have sent to logcat you will see that in each case a unique site was returned. But in general, a given name may correspond to more than one location. For example, if we enter the address 1101 Shevlin Drive (a location where I once lived), our messages sent to the logcat output with Log.i() give


06-15 13:16:19.592 I/Location-List( 8581): Raw String:
06-15 13:16:19.592 I/Location-List( 8581): Address[addressLines=[0:"1101 Shevlin Dr",1:"El Cerrito, CA
94530",2:"USA"],feature=1101,admin=California,sub-admin=Contra Costa,locality=El Cerrito,thoroughfare=Shevlin
Dr,postalCode=94530,countryCode=US,countryName=United
States,hasLatitude=true,latitude=37.91892,hasLongitude=true,longitude=-122.294027,phone=null,url=null,extras=null]
06-15 13:16:19.592 I/Location-List( 8581): Address[addressLines=[0:"1101 Shevlin Dr",1:"Wayzata, MN
55391",2:"USA"],feature=1101,admin=Minnesota,sub-admin=Hennepin,locality=Wayzata,thoroughfare=Shevlin
Dr,postalCode=55391,countryCode=US,countryName=United
States,hasLatitude=true,latitude=44.964079,hasLongitude=true,longitude=-93.5790595,phone=null,url=null,extras=null]

06-15 13:16:19.592 I/Location-List( 8581): Options:
06-15 13:16:19.592 I/Location-List( 8581): (1) 1101 Shevlin Dr, El Cerrito, CA 94530, United States
06-15 13:16:19.592 I/Location-List( 8581): (2) 1101 Shevlin Dr, Wayzata, MN 55391, United States

So we see that there are two options, one in El Cerrito, California, and one in Wayzata, Minnesota. With the way we have set the code up, it will always send the map to the last option (which is not my old address in this case; I lived in California, not Minnesota). We can see from this output that it would not be too difficult to use the information that we have extracted to present to the user a choice list (already contained in the array optionArray) when there is more than one option, so that the user can select the correct one. We leave that as an exercise.

 

Causing the Device to Track the Present Location

Now let's add the next activity, which will allow us to go to a screen displaying Google maps with our present location displayed, and track our location if it changes. This will require application of the mapping techniques that we have already employed, and the invocation of location services to determine the present position of the device.

We first modify the class MapMe so that it can implement a Google maps display centered on a specified latitude and longitude. Right-click on layout under res/layout and choose New > Android XML File to create an XML file of type Layout named mapme.xml in the folder /res/layout, giving it a LinearLayout root element. Then, edit the file mapme.xml to give


<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    
    <view android:id="@+id/mv2"
        class="com.google.android.maps.MapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1" 
        android:clickable="true"
        android:apiKey="myMapKey"
        />	
    </LinearLayout>

where you should substitute for "myMapKey" your apiKey, as described above. Note that we have to insert an apiKey for each MapView in the project. In this project we have two MapViews, one in MapMe and one in ShowTheMap. We have to insert the apiKey in the XML layout file for each (even though the key is the same in the two cases).

Now we must edit the file MapMe.java to do three things:

  1. Require that the class implement the public interface LocationListener:
    
        public class MapMe extends MapActivity implements LocationListener {
    
    
    which then requires that we add four methods that must be implemented by the user of the interface:
    
        @Override
        public void onLocationChanged(Location location) {
            // TODO Auto-generated method stub       
        }
    
        @Override
        public void onProviderDisabled(String provider) {
            // TODO Auto-generated method stub       
        }
    
        @Override
        public void onProviderEnabled(String provider) {
            // TODO Auto-generated method stub      
        }
    
        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            // TODO Auto-generated method stub       
        }
    
    
  2. Implement location services for the class so that we can determine the position of the device.

  3. Implement a Google MapView of the resulting position similar to what we have already seen, but now one that changes dynamically to follow the position of the device.

The listing of the complete MapMe class after doing all of this is


    package com.lightcone.mappingdemo;
    
    import android.content.Context;
    import android.location.GpsSatellite;
    import android.location.GpsStatus;
    import android.location.Location;
    import android.location.LocationListener;
    import android.location.LocationManager;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.KeyEvent;
    import android.view.Window;
    import android.widget.Toast;
    
    import com.google.android.maps.GeoPoint;
    import com.google.android.maps.MapActivity;
    import com.google.android.maps.MapController;
    import com.google.android.maps.MapView;
    
    public class MapMe extends MapActivity implements LocationListener {
	
	private static double lat;
	private static double lon;
	private MapController mapControl;
	private MapView mapView;
	LocationManager locman;
        Location loc;
        String provider = LocationManager.GPS_PROVIDER;
        String TAG = "GPStest";
	Bundle locBundle;
	int numberSats = -1;
	float satAccuracy = 2000;
	private float bearing;
	private double altitude;
	private float speed;
	private String currentProvider;
	
	// Following 2 parameters control how often the GPS is called to update location.
	// These are only suggestions to the hardware. Precision versus power-consumption 
	// considerations govern what these settings should be.  The defaults can be reset
	// in the preferences (settings) menu. (See the updateGPSprefs method below.)
	
	long GPSupdateInterval;         // In milliseconds
	float GPSmoveInterval;          // In meters
	
	
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);  // Suppress title bar to give more space
            setContentView(R.layout.mapme); 

            GPSupdateInterval = 5000;
            GPSmoveInterval = 1;
            
            // Set up location manager for determining present location of phone
            locman = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 
                
            // Listener for GPS Status...	
            final GpsStatus.Listener onGpsStatusChange = new GpsStatus.Listener(){
                public void onGpsStatusChanged(int event){
                    switch(event){
                        case GpsStatus.GPS_EVENT_STARTED:
                                // Started...
                        break;
                        case GpsStatus.GPS_EVENT_FIRST_FIX:
                                // First Fix...
                                Toast.makeText(MapMe.this, "GPS has 1st fix", Toast.LENGTH_LONG).show();
                        break;
                        case GpsStatus.GPS_EVENT_STOPPED:
                                // Stopped...
                        break;
                        case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
                                // Satellite update
                        break;
                    }
                    GpsStatus status = locman.getGpsStatus(null);
                    
                    // Not presently doing anything with following status list for individual satellites	
                    Iterable<GpsSatellite> satlist = status.getSatellites();
                }
            };
    
            locman.addGpsStatusListener(onGpsStatusChange);
            locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);  
            Log.i(TAG, locman.toString());
                
            // Add map controller with zoom controls
            mapView = (MapView) findViewById(R.id.mv2);
            mapView.setSatellite(false);
            mapView.setTraffic(false);
            mapView.setBuiltInZoomControls(true);   // Set android:clickable=true in main.xml
            int maxZoom = mapView.getMaxZoomLevel();
            int initZoom = (int)(0.95*(double)maxZoom);
            mapControl = mapView.getController();
            mapControl.setZoom(initZoom);
        }
	
	// This sets the s key on the phone to toggle between satellite and map view
	// and the t key to toggle between traffic and no traffic view (traffic view
	// relevant only in urban areas where it is reported). (See Murphy, pp. 304-305)
	// The map/satellite and traffic/no traffic toggles are independent so there are
	// four combinations.
	
	public boolean onKeyDown(int keyCode, KeyEvent e){
            if(keyCode == KeyEvent.KEYCODE_S){
                mapView.setSatellite(!mapView.isSatellite());
                return true;
        } else if(keyCode == KeyEvent.KEYCODE_T){
                mapView.setTraffic(!mapView.isTraffic());
                centerOnLocation();  // To ensure change displays immediately
        }
        return(super.onKeyDown(keyCode, e));
    }
	
    // Required method since class extends MapActivity
    @Override
    protected boolean isRouteDisplayed() {
        return false;  // Don't display a route
    }

    // Required method since class implements LocationListener interface
    @Override
    public void onLocationChanged(Location location) {
        // Called when location has changed
        centerOnLocation();
    }

    // Required method since class implements LocationListener interface
    @Override
    public void onProviderDisabled(String provider) {
        // Called when user disables the location provider. If 
        // requestLocationUpdates is called on an already disabled 
        // provider, this method is called immediately.
        	
    }

    // Required method since class implements LocationListener interface
    @Override
    public void onProviderEnabled(String provider) {
        // Called when the user enables the location provider
        
    }

    // Required method since class implements LocationListener interface
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        // Called when the provider status changes. This method is called 
        // when a provider is unable to fetch a location or if the provider 
        // has recently become available after a period of unavailability.
        
        centerOnLocation();	
    }
	
    // Method to query phone location and center map on that location
    private void centerOnLocation() {
    	loc = locman.getLastKnownLocation(provider);
    	if(loc != null){
            lat = loc.getLatitude();
            lon = loc.getLongitude();
            GeoPoint newPoint = new GeoPoint((int)(lat*1e6),(int)(lon*1e6)); 	   
            mapControl.animateTo(newPoint);
            getSatelliteData();
        }
    }
    
    // Method to determine the number of satellites contributing to the fix and
    // various other quantities.
    
    public void getSatelliteData(){
    	if(loc != null){
    		
            // Determine number of satellites used for the fix
            locBundle = loc.getExtras();
            if(locBundle != null){ 
                numberSats = locBundle.getInt("satellites",-1);
            }
	   	    
           // Following return 0 if the corresponding boolean (e.g., hasAccuracy) are false.
            satAccuracy = loc.getAccuracy();
            bearing = loc.getBearing();
            altitude = loc.getAltitude();
            speed = loc.getSpeed();		
        }
    }
}

The key player here is locman, which is an instance of LocationManager. It is not instantiated directly but rather is invoked through getSystemService(Context.LOCATION_SERVICE). locman adds a GpsStatus.Listener to listen for changes in GPS status and registers to receive location updates through the requestLocationUpdates method. The method centerOnLocation is used to retrieve a location loc by invoking the getLastKnownLocation method of locman and send the map to that location. It is invoked from both the onStatusChanged and onLocationChanged methods. The method getSatelliteData retrieves information about the quality of the satellite information and also bearing, speed, and altitude. The methods of the MapView class have been invoked by the mapView object to set up the map and its controllers.


We have simply specified GPS for the location provider in the above example, but there is a getBestProvider() method for Location Provider that can be invoked once a Criteria object has been created. This will choose a location provider based on whether altitude is required, the accuracy desired for position, and whether the user is willing to pay for location services. Android devices obtain precise locations from GPS fixes (the ACCESS_FINE_LOCATION permission in AndroidManifest.xml), but they may offer less precise locations from triangulation on cell phone towers and proximity to WIFI hotspots (the ACCESS_COARSE_LOCATION permission in AndroidManifest.xml). For some location service applications (for example, "What city are you near?" to determine nearby movie theaters, or "What country are you in?", to determine the default language to use in an application), coarse-grain location is sufficient. On the other hand, an application like turn-by-turn navigation for driving requires fine-grain location.

If after editing this class you execute the program on an actual device you should find that clicking the Track Present Location button opens a map screen that is centered on your present location (if the device is GPS enabled and receiving signals). On an emulator there is no GPS, but you can simulate positioning the device at specific GPS coordinates by one of the procedures described in the Appendix Simulating GPS Position. Once you have clicked the Track Present Location button on the virtual device, you should then see it move the center of the map display to the new simulated coordinates. With a real device having a functioning GPS receiver, you should find that the map display tracks your motion with the device.

 

Managing Resources with onPause() and onResume()

One thing that you will notice on a virtual or real device is that the GPS symbol at the top of the screen may remain on, even after you have quit using this app. This is because we still need to do some application lifecycle cleanup, which we might as well do right now.

The problem is that even when we "quit" the app, the operating system may very well just put it into the background instead of really killing it, if it decides it has plenty of resources available. Then, as presently written, MapMe will still be asking for position updates while in the background, which will cause the GPS to remain on when it could be turned off, and this will consume a lot of power. Let's fix that by adding the following methods to MapMe


    // OnPause() and onResume() methods to handle when app is forced to background by another
    // process and then resumed. Generally should release resources not needed while in background
    // and restore when resumed. For example, in the following locman.removeUpdates(this) when 
    // pausing removes the request for GPS updates when MapMe goes into the background. This
    // permits the GPS engine to shut down (if it is not being used by another program), which saves
    // a lot of power. If MapMe is resumed (moved from background to foreground), the onResume()
    // method resumes GPS update requests through the locman.requestLocationUpdates() method.

    public void onPause() {
        super.onPause();
        Log.i(TAG,"******  MappingDemo is pausing: Removing GPS update requests to save power");
        locman.removeUpdates(this);
    }
	
    public void onResume(){
        super.onResume();
        Log.i(TAG,"******  MappingDemo is restarting: Resuming GPS update requests");	   
        locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);	
    }

In addition, we modify the onProviderDisabled and onProviderEnabled methods of MapMe to


    @Override
    public void onProviderDisabled(String provider) {
        locman.removeUpdates(this);	
    }

    @Override
    public void onProviderEnabled(String provider) { 
        locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);
    }

The OnPause() and onResume() methods handle when the app is forced to background by another process and then resumed, while the onProviderDisabled and onProviderEnabled methods deal with loss and restoration of the GPS provider. Generally we should release resources not needed while in the background (or if there is no provider available) and restore when resumed. For example, locman.removeUpdates(this) when pausing removes the request for GPS updates when MapMe goes into the background. This permits the GPS engine to shut down, which saves a lot of power. If MapMe is moved from background to foreground, the onResume() method resumes GPS update requests through the locman.requestLocationUpdates() method. For more details, see the LocationListener documentation.

Now you should find that if the app moves to the background the GPS symbol rather quickly turns off (provided that something else on the device is not requesting location services), and if you restart the app the GPS symbol appears again rather quickly.

 

Adding An Options Menu

Let's go ahead at add the basics of the options menu. First, since one of the options is going to be to quit the application, let's add to MappingDemo.java the following method to exit


    public void finishUp(){
       finish();
    }


Next we add to strings.xml the following strings that we will need (inside <resource> </resource>)


    <string name="quit">Exit</string>
    <string name="help_title">Help</string>
    <string name="help_text">This is the help file for this application.
          \n\nItem 1\n<i>Item 2 (in italics) </i>\nItem 3</string>
    <string name="settings">Settings</string>

(The \n inserts a newline and the tag <i>item</i> puts the enclosed text in italics.) We are going to add icons to the options menu, so we must copy them into a resource directory. Let us assume that you have the icons stored somewhere as .png files. (Standard Android menu icons, including the ones we are going to use, may be found at the icon design page. You can also download them from the course image directory. The images that we will require are

.
Image resources for Android must be named by strict rules: you can use only lower-case letters a-z, numbers 0-9, _ and . in these image file names. The preferred format is .png, but .gif and .jpg can be used.

Right-click on MappingDemo/res/drawable-hdpi and select Import. On the resulting screen, select General > File System and click Next. Click the Browse button and navigate to the directory containing the icons you wish to add as resources. Click OK. Then on the resulting screen select the files to be imported, as in the following figure



(you can select all files in the directory by checking the box in the left window). Once the files are selected, click Finish. The new image files should now appear under res/drawable-hdpi.

Our options menu will have three functions: (1) Exit, which will be handled by the finishUp() method given above, (2) Help, and (3) Settings. The last two we will implement by using intents to launch new screens that will be defined by the Java classes to be specified in Help.java and Prefs.java. Let's create the stubs for the Help class, and the corresponding XML layout now. (The Prefs class is special and we will define it later.) Using techniques already described, create the layout file help.xml in the res/layout directory with content


  <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="10sp">
    <TextView
        android:id="@+id/help_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/help_text" />
    </ScrollView>

and create in the src/<packagename> directory the file Help.java with the content


    package com.lightcone.mappingdemo;
    import android.app.Activity;
    import android.os.Bundle;
    
    public class Help extends Activity {	
        @Override
        protected void onCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            setContentView(R.layout.help);
        }
    }

Now we are ready to create our options menu. A Menu can be instantiated from an XML file using MenuInflater. However, in this example we are going to create the menu and its event handlers directly in Java code. First add to the class MappingDemo the imports


    import android.view.Menu;
    import android.view.MenuItem;

then add the following menu-creation method to MappingDemo


    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        super.onCreateOptionsMenu(menu);
        int groupId = 0;
        int menuItemOrder = Menu.NONE;
        // Create menu ids for the event handler to reference
        int menuItemId1 = Menu.FIRST;
        int menuItemId2 = Menu.FIRST+1;
        int menuItemId3 = Menu.FIRST+2;
        // Create menu text
        int menuItemText1 = R.string.quit;
        int menuItemText2 = R.string.help_title;
        int menuItemText3 = R.string.settings;
        // Add the items to the menu
        MenuItem menuItem1 = menu.add(groupId, menuItemId1, menuItemOrder, menuItemText1)
                .setIcon(R.drawable.ic_menu_close_clear_cancel);
        MenuItem menuItem2 = menu.add(groupId, menuItemId2, menuItemOrder, menuItemText2)
                .setIcon(R.drawable.ic_menu_help);
        MenuItem menuItem3 = menu.add(groupId, menuItemId3, menuItemOrder, menuItemText3)
                .setIcon(R.drawable.ic_menu_preferences);
        return true;
    }

and add the following method to MappingDemo to handle events associated with this menu


    // Handle events from the popup menu above   
    public boolean onOptionsItemSelected(MenuItem item){
        super.onOptionsItemSelected(item);
        switch(item.getItemId()){
            case (Menu.FIRST):
                finishUp();
                return true;
            case (Menu.FIRST+1):
                // Actions for help page
                Intent i = new Intent(this, Help.class);
                startActivity(i);
                return true;
            case(Menu.FIRST+2):
                // Actions for settings page
                // These actions will be filled in later

                return true;             
        }
        return false;
    }

(see Creating Menus and the discussion of Menus in Android User Interfaces for more details). Finally, add the corresponding intent declaration to the AndroidManifest.xml file:


    <activity android:name=".Help" android:label="Help"> </activity>

(within the <application></application> tag). Now if you execute the program and click the MENU button on the emulator or phone you should get a popup options menu at the bottom of the screen, as in the following figure.



If you select Exit from this menu the app should exit, and if you select Help from the menu you should get the display shown in the following figure,



which is showing the title corresponding to the string help_title from strings.xml and displaying the text given by the string help_text in strings.xml. We will leave it to the user to edit the text in help_text to produce an appropriate help file, but before moving on let's prettify the formatting of the Help screen a little. The screen displayed in the preceding figure is serviceable but rather blah. Let's invoke a theme, as described in Android User Interfaces to make it look a little nicer. Edit the AndroidManifest.xml file and change the activity declaration for .Help to add a Theme.Dialog theme:


    <activity android:name=".Help" android:label="Help" 
        android:theme="@android:style/Theme.Dialog"> </activity>

Now if you press Help in the options menu you get the following display



Selecting Settings from the options menu doesn't do anything yet; we will take care of that a little later.

 

Adding Map Overlays

One of the most important advantages of embedding Google maps in an application using MapView instead of just using the map application built into the phone is that you have control of what to do with it and one of the most important things you can do with it is to use the Overlay class of com.google.android.maps to overlay symbols and graphics on the map to indicate information, position, orientation, routes, and so on, and those symbols can be programmed so that actions are initiated when they are touched. Let's see how to modify MappingDemo to do that.

First, there is a built-in class at MyLocationOverlay that implements two special overlays rather automatically in conjunction with MapView: (1) a compass rose showing the current orientation of the device, and (2) a pulsing blue symbol that represents your current position, with a lighter-colored radius indicating the uncertainty in that position. Let's add this capability to our GPS tracking map. To implement it we could simply instantiate MyLocationOverlay. However, we do something a little more sophisticated: we first subclass (extend) it, because we are going to override its dispatchTap() method so that we can use the current position symbol to respond to tap events and initiate actions.

Right-click on MappingDemo/<package-name> and select New > Class. In the resulting dialogue window give the new class the name MyMyLocationOverlay, specify the Superclass as com.google.android.maps.MyLocationOverlay, and click Finish. Edit the resulting file MyMyLocationOverlay.java so that it reads


    package com.lightcone.mappingdemo;
    import android.content.Context;
    import android.widget.Toast;
    import com.google.android.maps.MapView;
    import com.google.android.maps.MyLocationOverlay;
    
    // This class subclasses (extends) MyLocationOverlay so that 
    // we can override its dispatchTap method
    // to handle tap events on the present location dot.
    
    public class MyMyLocationOverlay extends MyLocationOverlay {
    
        private Context context;
    
        public MyMyLocationOverlay(Context context, MapView mapView) {
            super(context, mapView);
            this.context = context;   
        }
            
        // Add stub to Override the dispatchTap() method. Will fill in below. 
        
        @Override
        protected boolean dispatchTap(){
            // More to add later
            return true;
        }
    }

Edit MapMe.java and add a class variable


    private MyMyLocationOverlay myLocationOverlay;

and add within the method onCreate after the code implementing the map controller in MapMe.java


    // Set up compass and dot for present location map overlay
    List<Overlay> overlays = mapView.getOverlays();
    myLocationOverlay = new MyMyLocationOverlay(this,mapView);
    overlays.add(myLocationOverlay);   

which will require adding the imports


    import java.util.List;
    import com.google.android.maps.Overlay;

and finally modify the onPause and onResume methods in MapMe.java to read


    public void onPause() {
        super.onPause();
        Log.i(TAG,"******  MapMe is pausing: Removing GPS update requests to save power");
        myLocationOverlay.disableCompass();
        myLocationOverlay.disableMyLocation();
        locman.removeUpdates(this);
    }
    
    public void onResume(){
        super.onResume();
        Log.i(TAG,"******  MapMe is restarting: Resuming GPS update requests");	   
        locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);	
        myLocationOverlay.enableCompass();
        myLocationOverlay.enableMyLocation();
    }

(The enable/disable statements in the onPause and onResume methods are necessary because the MyLocationOverlay requests location updates and we want this to occur only when the activity is in the foreground, not when it is in the background.)

Now if you execute MappingDemo on an actual device with active location services, or an emulator with a simulated position, and select Track Current Location you should see a pulsing blue circle at your current position and a compass rose indicating an orientation of the device relative to the map. The following figure illustrates for an actual device (Motorola Backflip phone running Android 1.5),




and the next figure illustrates the same location but with the phone rotated by about 90 degrees to the right relative to the first figure.



When tested on an emulator running Android 1.6 with Google maps, the pulsing blue dot displayed for only about 30 seconds after sending the emulator to a simulated position as described above, and the compass rose did not display at all, so you may need an actual device to fully test this capability.

In addition to these custom overlays already included in the android API with mapping, there are two additional types of map overlays that are extremely useful. The first uses the class ItemizedOverlay to overlay a list of symbols with labels at specified positions. This, for example, is useful to indicate visually the location of a list of things (for example, ice cream shops or pubs), and event handlers can be attached to the symbols so that actions such as popping up information windows can be initiated when users touch the symbols. The second uses the class Overlay to create an overlay onto which custom graphics can be drawn by the user. This is useful, for example, if we wish to indicate a route between two points on the map under program control. Below we will illustrate how to implement this second type of overlay and in Map Overlay Demo we will show how to use an ItemizedOverlay.

 

Setting Up a Preferences Screen

Since specifying settings (preferences or options) for a program is a rather standard procedure for most modern programs running on desktop computers, it should not be surprising that a sophisticated operating system like Android provides a built-in mechanism for creating preference screens and processing the information associated with changes in settings made by users. This is implemented through the Android classes PreferenceActivity and Preference (see also this preferences example and this tutorial).

Let's use these classes to give the user some options for setting preferences in MappingDemo. We already put in a Settings (Preference) option in the options menu above with a stub for the corresponding event handler, so we just need to create the screen that this action will display and fill out the event handler. First let's define some new strings we will need. Edit strings.xml to add


    <string name="compass_title">Compass</string>
    <string name="compass_summary">Whether to display orientation compass</string>

inside the resources tag.

In Eclipse, if the folder MappingDemo/res/xml does not exist, create it by right-clicking on MappingDemo/res/, selecting New > Folder, setting Folder Name to "xml", and clicking Finish. Now right click on MappingDemo/res/xml and select New > Android XML File. On the resulting screen give prefs.xml for the File Name, Select "Preference" for the type of layout, and click Finish. Edit the file prefs.xml (note that this special layout file is in res/xml, unlike our normal layout files that are in res/layout) so that it reads


<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
  <CheckBoxPreference
    android:key = "compass"
    android:title = "@string/compass_title"
    android:summary = "@string/compass_summary"
    android:defaultValue = "true" />
</PreferenceScreen>

(You may have to click the prefs.xml tab at the bottom of the screen to display the text of the XML file.)

Now create a new Java class by right-clicking on MappingDemo/src/<packagename>, selecting New > Class, setting Name to Prefs, Modifier to public, Superclass to android.preference.PreferenceActivity, and click Finish. Edit the resulting file Prefs.java to read


    package com.lightcone.mappingdemo;
    import android.os.Bundle;
    import android.preference.PreferenceActivity;
    public class Prefs extends PreferenceActivity {
        protected void onCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.prefs);
        }
    }

Register the Prefs activity by adding to AndroidManifest.xml


    <activity android:name=".Prefs" android:label="Prefs"></activity>

inside the applications tag, and edit the onOptionsItemSelected method of MappingDemo.java to add an intent launching the Prefs class when the Settings button is clicked:


    public boolean onOptionsItemSelected(MenuItem item){
        super.onOptionsItemSelected(item);
        switch(item.getItemId()){
            case (Menu.FIRST):
                finishUp();
                return true;
            case (Menu.FIRST+1):
                // Actions for help page
                Intent i = new Intent(this, Help.class);
                startActivity(i);
                return true;
            case(Menu.FIRST+2):
                // Actions for settings page
                Intent j = new Intent(this, Prefs.class);
                startActivity(j);
                return true;	
        }
        return false;
    }

Now if you execute the program, click the MENU button, and select Settings you should get the screen displayed in the following figure,





with a checkbox that will toggle on and off when clicked. This checkbox is intended to give the user control over whether the orientation compass is displayed when the map in MapMe showing current location is displayed. Let's now add event handling that accomplishes this.

The Android class to handle events if the preferences are changed is OnSharedPreferenceChangeListener and we add to the class definition in Prefs.java "implements OnSharedPreferenceChangeListener", which necessitates adding a couple of new imports and a new method onSharedPreferenceChanged. After the required changes Prefs.java has the content


package com.lightcone.mappingdemo;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.util.Log;

public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener {
	
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.prefs);
        
        // Register a change listener
        
        Context context = getApplicationContext();
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        prefs.registerOnSharedPreferenceChangeListener(this);
    }

    // Inherited abstract method so it must be implemented
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {	
        Log.i("Preferences", "Preferences changed, key="+key);
    }
    
    // Static method to return the preference for whether to display compass
    public static boolean getCompass(Context context){
        return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("compass", true);
    }
}

The String variable key indicates which element on the preferences page has changed. Of course at the moment the id label android:key = "compass" in prefs.xml is the only element in prefs.xml, but shortly we will add other elements to prefs.xml and the value of key will then allow us to determine which preference has been changed.

Finally, we modify the onPause and onResume methods in MapMe.java to incorporate a logical decision based on the value returned by the Prefs.getCompass (context) method concerning whether to display the compass:


    public void onPause() {
        super.onPause();
        Log.i(TAG,"******   MapMe pausing: Removing GPS update requests to save power");
        if(Prefs.getCompass(getApplicationContext())) myLocationOverlay.disableCompass();
        myLocationOverlay.disableMyLocation();
        locman.removeUpdates(this);
    }
            
    public void onResume(){
        super.onResume();
        Log.i(TAG,"******  MapMe restarting: Resuming GPS update requests");
        locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);	
        if(Prefs.getCompass(getApplicationContext())) myLocationOverlay.enableCompass();
        myLocationOverlay.enableMyLocation();
    }

Now if you execute this on a real device you should be able to toggle the compass rose on and off in MapMe by changed the preferences setting.

 

Adding Options for GPS Precision and Power Consumption

Let's now add to the preferences user options for trading off position precision against power consumption. First edit strings.xml and add the lines


    <string name="gpsPrefs_title">GPS Preferences</string>
    <string name="gpsPrefs_summary">Location precision (higher precision gives shorter battery life)</string>

Next, we create an XML file to store array data. From Eclipse, right-click on MappingDemo/res/values and select New > Android XML File. Give the file the name arrays.xml, check that the type is Values and that the Folder is res/values, and click Finish. Edit the resulting file arrays.xml to give


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <string-array name="updateOptions">
            <item>Highest Precision</item>
            <item>Medium Precision</item>
            <item>Lowest Precision</item>
        </string-array>
        
        <string-array name="updateIndex">
            <item>1</item>
            <item>2</item>
            <item>3</item>
        </string-array>
    </resources>

(Note that it is critical that the second array above be a string-array and not just an array---see post1 and post2. Otherwise, at least for Linux, you will get strange error messages.)

Now edit prefs.xml to add a new ListPreference option:


  <?xml version="1.0" encoding="utf-8"?>
    <PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android">
    
    <CheckBoxPreference
        android:key = "compass"
        android:title = "@string/compass_title"
        android:summary = "@string/compass_summary"
        android:defaultValue = "true" />
            
    <ListPreference
        android:title="@string/gpsPrefs_title"
        android:summary="@string/gpsPrefs_summary"
        android:key="gpsPref"
        android:defaultValue="1"
        android:entries="@array/updateOptions"
        android:entryValues="@array/updateIndex" /> 
 
    </PreferenceScreen>

Because these are shared persistent preferences, if you make a programming error in setting up preferences you may end up storing erroneous preferences data and may have to wipe user data on either a device or an emulator to get it to not repeat the error after you have corrected it. If you encounter this problem for a device, you can uninstall the program with

   adb -d uninstall com.lightcone.mappingdemo

(assuming it is the only device attached; otherwise you will have to use -s to target its serial number), and then execute the corrected program. For an emulator you can issue a similar command

   adb -e uninstall com.lightcone.mappingdemo

(assuming it is the only emulator running), or you can kill the emulator, restart it using Window > Android SDK and AVD Manager, select the emulator and click Start, check the Wipe User Data checkbox, and then click Launch.

Now if you execute the program and click the MENU button and choose Settings, you should get the screen in the following figure,





and if you click GPS Preferences you should get a screen like this figure.





If you click a different option on that screen than the current one and then click GPS Preferences again you should see that the option has changed. Thus, Android is storing and managing shared preferences that can control the requested precision of the location services. All that remains is to implement some code to act on those preferences. Open Prefs.java and add the static method getGPSPref:


    // Static method to return the preference for the GPS precision setting
    public static String getGPSPref(Context context){
        return PreferenceManager.getDefaultSharedPreferences(context).getString("gpsPref", "1");
    }

then open MapMe.java and add the method


    // Method to assign GPS prefs
    public void updateGPSprefs(){
        int gpsPref = Integer.parseInt(Prefs.getGPSPref(getApplicationContext()));
        switch(gpsPref){
        case 1:
            GPSupdateInterval = 5000;  // milliseconds
            GPSmoveInterval = 1;       // meters
            break;
        case 2:
            GPSupdateInterval = 10000;
            GPSmoveInterval = 100;
            break;
        case 3:
            GPSupdateInterval = 125000;
            GPSmoveInterval = 1000;
            break;
        }	
    }

modify the onResume() method of MapMe to call this new method:


    public void onResume(){
        super.onResume();
        // Check for GPS prefs and set parameters accordingly
        updateGPSprefs();
        locman.requestLocationUpdates(provider,GPSupdateInterval,GPSmoveInterval,this);	
        if(Prefs.getCompass(getApplicationContext())) myLocationOverlay.enableCompass();
        myLocationOverlay.enableMyLocation();
        Log.i(TAG,"******  MapMe restarting: Resuming GPS update requests."+
            " GPSUpdateInterval="+GPSupdateInterval+"ms GPSmoveInterval="+GPSmoveInterval+" m");
    }

and remove the assignments of GPSUpdateInterval and GPSmoveInterval in the onCreate method of MapMe, replacing them by a call to updateGPSprefs():


    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);  // Suppress title bar to give more space
        setContentView(R.layout.mapme); 
        
        updateGPSprefs();
        
        // Set up location manager for determining present location of phone
 	    locman = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 
    .
    .
    .

Now if you run the program you should be able to reset the GPS preferences to any of the three possibilities and if you then click the Track Present Location button the logcat output from the Log.i statement in updateGPSprefs should confirm that the GPS precision parameters have been reset. Since in Android shared preferences are persistent once stored, these preferences should carry over to new sessions with the application until they are changed.


The preceding example of setting user preferences to trade off GPS performance versus power consumption is a nice example of how to implement a potentially useful option. However, on actual devices these particular GPS setting requests might be overridden by settings baked in by the manufacturer or carrier. For example, when tested on an AT&T Motorola Backflip and an AT&T Samsung Captivate (Galaxy S) I could find no significant difference in GPS behavior for these different settings.

While we are at it, let's illustrate another type of shared preference input option, the <EditTextPreference></EditTextPreference> tag, which allows editable text input for a preference parameter. Open prefs.xml and add an EditTextPreference tag as follows


  <?xml version="1.0" encoding="utf-8"?>
    <PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android">
    
    <CheckBoxPreference
        android:key = "compass"
        android:title = "@string/compass_title"
        android:summary = "@string/compass_summary"
        android:defaultValue = "true" />
            
    <ListPreference
        android:title="@string/gpsPrefs_title"
        android:summary="@string/gpsPrefs_summary"
        android:key="gpsPref"
        android:defaultValue="1"
        android:entries="@array/updateOptions"
        android:entryValues="@array/updateIndex" />
        
    <EditTextPreference
        android:name="EditText Preference"
        android:summary="This allows you to edit the name"
        android:defaultValue=""
        android:title="Edit the Name"
        android:key="editTextPref" />
   
    </PreferenceScreen>

Then if you open the Options menu by clicking the MENU button and then Settings, you should see a display as in the following figure.





If you select the "Edit the Name" option, you should get a screen similar to the following figure,





with an editable text field that you can change and save. To access and use this preference, add the following static method to Prefs.java


    // Static method to return the preference for the name (only used for demonstration)
    public static String getTitle(Context context){
        return PreferenceManager.getDefaultSharedPreferences(context).getString("editTextPref", "");
    }

We won't do anything with this except to illustrate that the edited preference has indeed changed by modifying the onSharedPreferenceChanged method of Prefs.java to


    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {	
        Log.i("Preferences", "Preferences changed, key="+key);
        if(key.compareTo("editTextPref")==0)
        Log.i("Preferences", " Changed name to " + Prefs.getTitle(getApplicationContext()));
    }

Now if you change the name in the EditTextPreference field on the Options menu, the logcat output will confirm the change.

 

Adding a Display Overlay

We can subclass com.google.android.maps.Overlay to allow us to put text and graphics into an overlay layer on a map. Let's illustrate by adding to the upper part of the MapMe map display a readout of the current latitude and longitude for the device, the number of satellites being tracked for the GPS fix, the corresponding accuracy in meters, the altitude, the speed, and the bearing. We shall also implement the capability to toggle this display on and off by tapping on the present-location symbol (pulsing blue dot) on the map display.

Right-click on MappingDemo/<packagename> and select New > Class. On the resulting screen set Name to DisplayOverlay and Superclass to com.google.android.maps.Overlay, and click Finish. Edit the resulting file DisplayOverlay.java to give


    package com.lightcone.mappingdemo;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import com.google.android.maps.MapView;
    import com.google.android.maps.Overlay;
    
    public class DisplayOverlay extends Overlay {	
        private Paint paint;
        private double lat;
        private double lon;
        private double satAccuracy;
        private int numberSats;
        private float bearing;
        private double altitude;
        private float speed;
        private String currentProvider;
        public static boolean showData = true;
        
        @Override
        public void draw(Canvas canvas, MapView mapview, boolean shadow) {
            super.draw(canvas, mapview, shadow);
            if(showData){
                paint = new Paint();
                paint.setAntiAlias(true);
                paint.setARGB(80,255,255,255);
                canvas.drawRect(0,0,350,33,paint);
                paint.setTextSize(11);
                paint.setARGB(180, 0, 0, 0);
                canvas.drawText("Lat = "+lat+"  Long = "+lon+"  Alt = "+(int)altitude+" m", 8, 14, paint);
                canvas.drawText("Sat = "+numberSats+" Accur = "+(int)satAccuracy+" m"
                    +" speed = "+(int)speed+" m/s  bearing = "+(int)bearing+" deg", 8, 27, paint);
            }
        }
            
        // Method to insert updated satellite data
        public void putSatStuff(double lat, double lon, double satAccuracy, float bearing, double altitude,
            float speed, String currentProvider, int numberSats){
            this.lat = lat;
            this.lon = lon;
            this.satAccuracy = satAccuracy;
            this.bearing = bearing;
            this.altitude = altitude;
            this.speed = speed;
            this.currentProvider = currentProvider;
            this.numberSats = numberSats;	
        }
    }

In this code we have overriden the draw method of Overlay to draw on a Canvas using style and color attributes specified by the Paint class, and added a method putSatStuff to insert into DisplayOverlay the satellite data that was retrieved in MapMe.

Now we modify MapMe.java. First we add class variables displayOverlay and mapOverlays,


   public DisplayOverlay displayOverlay;
   private List<Overlay> mapOverlays;

and then add at the end of the onCreate method


    // Set up overlay for data display
    displayOverlay = new DisplayOverlay();
    mapOverlays = mapView.getOverlays();
    mapOverlays.add(displayOverlay);

and then add at the end of the MapMe method centerOnLocation


    if(displayOverlay != null){
        displayOverlay.putSatStuff(lat, lon, satAccuracy, bearing, 
           altitude, speed, currentProvider, numberSats);
    }

Now if you run the application you should have displayed at the top left the latitude/longitude and satellite information. The following image





illustrates for a Motorola Backflip phone running Android 1.5 (in this case we have used the preferences defined earlier to suppress the compass rose). For this example the phone was stationary and indoors, so the speed is zero, the bearing is not meaningful, and the GPS fix is very poor, as indicated by the large value displayed for the accuracy and the large error circle around the pulsing blue point. When this device is outdoors and has a clear view of the sky it is typically tracking 7 to 12 satellites with 5-10 meter accuracy, so then the error circle is comparable to the size of the position dot.

 

Tap Events on the Present-Location Dot

Let's finish this project by adding the capability to toggle the display of satellite data on and off by tapping on the current location dot. Edit MyMyLocation.java and fill out the dispatchTap() method to read


    // Override the dispatchTap() method to toggle the data display on and off when
    // the present location dot is tapped. Also display a short Toast (transient message) to the
    // user indicating the display status change.
    
    @Override
    protected boolean dispatchTap(){

        if(DisplayOverlay.showData){ 
            Toast.makeText(context, "Suppressing data readout", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context,"Display data readout", Toast.LENGTH_SHORT).show();
        }
        // Toggle the GPS data display
        DisplayOverlay.showData = ! DisplayOverlay.showData;

        return true;
    }

Now you should be able to toggle the satellite data display overlay on and off by tapping on the present-location dot, and a short transient message (a Toast) should appear on the screen indicating that the display status is changing when you do so.


The complete project for the application described above is archived at the link MappingDemo.



Exercises

1. Our current data readouts are in metric units. Add a preferences checkbox in the settings menu that allows the user to toggle between metric and English (feet and miles) units, and make the corresponding changes in the data readouts.

2. The Geocoder example was implemented on the main UI thread. Write a new version that subclasses AsyncTask to run this task asynchronously (AsyncTask is an abstract class that must be subclassed to be used). An asynchronous task is defined to be one that runs on a background thread but publishes its results on the main UI thread. The class AsyncTask permits this to be done in rather blackbox fashion, since it performs background operations and publishes results on the UI thread without the user having to manipulate threads and/or handlers directly (that is being done for you under the hood). Of course, you can also do this using using standard Java threading classes if you choose. Hint: See Map Overlay Demo for an example of using AsyncTask.

3. In the Geocoder example we caught only one exception (IOException) and did not do anything substantial with it. Modify the code to use exception handling to make the user experience more seamless and foolproof.

4. In the Geocoder example we stored multiple matches in the array optionArray, but displayed a map of only the last match in the list by default. Modify the code so that when there is more than one potential match the list of possible matches is presented to the user and when one of them is selected ShowTheMap displays that location.


Previous  | Next  | Home