For this lab you will be developing a new GPS recording application called WalkAbout. The purpose of the application is to allow users to record their GPS location information as they travel. While the application records the user's GPS data, it displays it back to the user in the form of a path drawn on top of a Google Map. While recording data, the user can launch a Camera activity that will capture and store pictures on an SD-Card. When finished recording, the application gives the user the option of storing the current GPS data as a private application file to be loaded and displayed at a later time.
At the end of this lab you will be expected to know:
- How to incorporate Google Maps into an application.
- How to register for and receive GPS location information.
- How to draw graphics on the screen using the Canvas class.
- How to create Google Maps Overlays.
- How to use the Camera.
- How to write data to the SD card.
- How to create and delete private application files.
- How to launch and receive results from Activities.
For this lab you will be working with a brand new application, completely independent of the previous labs. Over the course of the lab, you will be iteratively refining and adding functionality to the WalkAbout app. With each iteration you will be improving upon the previous iteration's functionality. You'll start by setting up and familiarizing yourself with the Eclipse project. You will then register for a Google Maps API key and begin incrementally developing the main map-viewing Activity. These first few exercises will have you display a map and the user's current position. Next, you will add functionality to record the user's GPS location by registering for and receiving data from what is known as the GPS Location Provider. After that, you will implement a Camera activity for taking pictures and saving them to the SD-Card. In the final section, you will save a user's GPS path to a file that is private to the application, and allow the application to restore itself from the file as well.
You will be given a Skeleton Project to work with. This project contains all of the java and resource files you will need to complete the lab. Some method stubs, member variables, and resource values and ids have been added as well. It is important that you not change the names of these methods, variables, and resource values and ids. These are given to you because there are unit tests included in this project as well that depend on these items being declared exactly as they are. These unit tests will be used to evaluate the correctness of your lab. You have complete access to these test cases during development, which gives you the ability to run these tests yourself. In fact, you are encouraged to run these tests to ensure that your application is functioning properly.
1. Setting Up...
1.1 Creating the Project
To begin, you will need to download and extract the skeleton project for the WalkAbout application.
Extract the project, making sure to preserve the directory structure.
Take note of the path to the root folder of the skeleton project.
Next you will need to setup an Android project for this app. Since the skeleton project was created in Eclipse, the easiest thing is to import this project into Eclipse
- Select File -> Import.
- In the Import Wizard, expand General and select Existing Projects into Workspace. Click Next.
- In the Import Project wizard, click select root directory and click Browse. Select the root directory of the skeleton project that you extracted. Click Open and then Finish.
- Click on the project name in the Package Explorer. Select File -> Rename and change the name of your project to lab5<userid> where <userid> is your user id (e.g. jsmith).
Next you will need to set the correct build target. In the Android SDK and AVD Manager
(available from Windows
menu in Eclipse), ensure that you have Google APIs for API 4. If you don't click on Available Packages (see the lecture notes or here
for further information).
Once you have the Google APIs add-on install, you need to use it as the target for your project. Right-click on the lab5<userid> project, the select Properties. Select Android and select Google APIs for 1.6 (API 4) as the Project Build Target.
1.2 Familiarize Yourself with the Project
The project contains three java class files and a single XML layout file which you will have to implement. WalkAbout.java will contain the definition for the main WalkAbout Activity class. This is the class that will display the map and the user's recorded path. The WalkAbout class makes use of a very simple XML layout file called map_layout.xml which you will have to fill in.
From the WalkAbout Activity you will be able to launch the CameraPreview Activity, which will be defined in the CameraPreview.java file. The CameraPreview Activity class displays a live camera preview on the screen and will allow the user to capture and save a picture to the SD-Card. This file was borrowed from the Google API's Demo application and the logic to display the camera preview is already implemented for you. You will have to fill in the rest.
The WalkAbout Activity class will rely on the PathOverlay class to draw the user's traveled path on its map. You will implement the logic needed for drawing the path in the PathOverlay.java file.
A general class diagram for the project is depicted below. Classes you will be implementing are colored yellow, the Google Maps API classes that you will be working with are colored in blue, and the standard Android classes you should be familiar with are colored in white.
2. Using Google Maps
In this section of the lab you will be working extensively with the Google Maps package. The Google Maps package allows you to include and manipulate Maps in your Android Applications. The general strategy for displaying a map in your application is to have an entire Activity dedicated to viewing a map. This activity must extend
class, which takes care of all the intricacies involved in setting up and tearing down the services required to support displaying a map.
The actual map that gets displayed is an instance of the com.google.android.maps.MapView
class, which extends the standard Android ViewGroup class. This class encapsulates all the gesture logic necessary for handling panning, zooming, and touching objects on the map. The map is just a Google map and can be displayed in three different modes: satellite, street, and traffic. In order to use this class you will need a Google Maps API Key. We will go over how to do this later.
By making use of the com.google.android.maps.Overlay
class you can overlay
interesting data on top of your map. By either extending this base class or using other overlay classes you can add another interaction layer onto your map. The com.google.android.maps.MyLocationOverlay
class is one such example; it can draw a beacon on the map to display the device's current position, and it can draw a compass on the map to display the device's current heading.
2.1 Displaying a Map
You will begin by implementing the functionality necessary to display a full-screen map in the WalkAbout Activity class. The layout for the WalkAbout Activity should be specified in the res/layout/map_layout.xml file, which you will have to fill in. This will require you to use the com.google.android.maps.MapView class. In order to do this, you will have to register the debug keystore (that Eclipse uses to run your applications) with Google in order to receive a Maps API Key. When finished, your application should appear as depicted in the figure below:
2.1.1 Get a Google Maps API Key.
In order to use the Google Maps API's and classes you will have to register the keystore you use to sign your application with the Google Maps Service. Once your keystore is registered, you will be provided with a Google Maps API key that can be used with any application signed by your keystore.
Every distribution of the Android Development Toolkit comes with a debug keystore that is used to sign your application when it is launched and run from Eclipse. This is how you are able to run your application on a device or emulator without signing it yourself. For the purposes of this lab, you need only to register your debug keystore. If you end up releasing an application to the market you will need to register an actual keystore.
2.1.2 Fill in the MapView XML Layout File
The main WalkAbout MapActivity class uses a very simple layout consisting of a root ViewGroup that contains only a single MapView element. Implement this layout by filling in the map_layout.xml file:
The WalkAbout MapActivity will display the MapView. All initialization relating to the layout should be done in WalkAbout.initLayout(), which gets called by onCreate():
- Inflate your map_layout.xml file.
- Initialize the MapView m_vwMap member variable by retrieving a reference to the MapView element in the XML layout file.
- Force the Zoom Controls to be displayed in by making a call to MapView.setBuiltInZoomControls(...) (hint: you don't actually type MapView).
You should be able to run your application and see a Google Map. When you touch the map, the Zoom Controls should be displayed centered along the bottom of the screen.
2.2 Adding Map Overlays
As stated before, Overlays are used when you want to draw objects on top of a MapView. Such objects could represent pin points for restaurant locations, a route for displaying directions, or a simple logo. Each MapView instance is responsible for drawing the overlays that get displayed on top of it. As such, each MapView instance has a list of all Overlays that it should draw. When you want to add an Overlay to a MapView, you simply retrieve the MapView's list of Overlays and add to it.
The Overlay objects are different from regular View objects in that they are not requested to be drawn by the system. Instead they are requested to be drawn by the MapView when the system requests that the MapView draw itself. As such, when you make changes to an Overlay, you cannot explicitly request that the overlay be re-drawn by calling a method like View.invalidate() or View.postInvalidate(). In order to get the Overlay objects to be redrawn you must call View.invalidate() or View.postInvalidate() on the MapView that owns the Overlay objects.
You will now add an Overlay to your current MapView which will display the device's current location and heading on top of the MapView. When finished, your application should appear as depicted in the figure below:
2.2.1 Display Your Location
In order for your application to display your location, it must access Positioning data from a Location Provider. In order to do this, it must declare that it uses the ACCESS_FINE_LOCATION permission in its manifest.
Enable the map to display your current location and heading by initializing and adding a com.google.android.maps.MyLocationOverlay to your MapView. This should be done in the WalkAbout.initLayout() method:
- Initialize m_locationOverlay with a new MyLocationOverlay object.
- Enable the MyLocationOverlay object's Compass and MyLocation beacon by using the MyLocationOverlay.enableCompass() and MyLocationOVerlay.enableMyLocation() methods.
- It is important to note that the MyLocationOverlay depends on the GPS or Network Location settings being enabled. These methods will do nothing if you do not have either the Network or GPS Location setting enabled.
- Retrieve m_vwMap's list of attached Overlay objects by calling its getOverlays() method.
- Add the m_locationOverlay to the list of Overlay objects.
You should be able to run your application and see a Google Map containing a beacon pointing out your current location and a compass displaying your current heading.
2.3 Initialize the WalkAbout Options Menu
The WalkAbout Activity has an Options Menu that will display five different MenuItems. Create the Options Menu as follows: Do not worry about setting OnMenuItemClickListeners yet, you will do that later.
- "Start/Stop": This MenuItem acts as a toggle for either starting or stopping GPS Location recording. This recording is a record of the path that you have traveled. While recording, the MenuItem should display "Stop." While stopped, the MenuItem should display "Start."
- Initialize the text for the MenuItem with R.string.startRecording resource string.
- Later on, you will dynamically update the text to reflect the current recording state.
- Use WalkAbout.STARTSTOP_MENU_ITEM constant as the Id for the MenuItem.
- "Save": This MenuItem will save the current recorded path.
- Initialize the text for the MenuItem with R.string.save.
- Use the WalkAbout.SAVE_MENU_ITEM constant as the Id for the MenuItem.
- "Load": This MenuItem loads the last saved path.
- Initialize the text for the MenuItem with R.string.load.
- Use the WalkAbout.LOAD_MENU_ITEM constant as the Id for the MenuItem.
- "Take Picture": This MenuItem launches a Camera Activity that allows you to take pictures.
- Initialize the text for the MenuItem with R.string.takePicture.
- Use the WalkAbout.PICTURE_MENU_ITEM constant as the Id for the MenuItem.
- "Enable GPS": This MenuItem launches the Device Settings Activity that allows you to enable the GPS Provider.
- Initialize the text for the MenuItem with R.string.enableGPS.
- Use the WalkAbout.ENABLEGPS_MENU_ITEM constant as the Id for the MenuItem.
3. Using Location Services
The Android System provides services for determining the current location of the device. The framework for working with these location based services lives under the android.location
package. A number of useful classes live inside this package:
- LocationManager: Provides an interface for the location based services. You will be interacting with this class most of the time when trying to obtain location information.
- LocationProvider: LocationProviders are classes that provide updates on the current location of the device. There exists a LocationProvider for each different technology that determines location. There exists a GPS LocationProvider and a Network LocationProvider. Each LocationProvider specifies criteria that must be satisfied in order for it to be used. For example, the GPS LocationProvider requires that the device have GPS hardware and that it be enabled.
- Criteria: This class allows you to programmatically outline criteria you would like from a LocationProvider. Such criteria may include accuracy, power consumption, altitude reporting, speed reporting, monetary cost, etc. You can then setup and give an instance of this class to the LocationManager and allow it to choose the LocationProvider that best matches your criteria.
- LocationListener: This is an interface that defines call backs for different events that are generated by LocationProviders. These can be registered with a particular LocationProvider to receive updates when the location changes or the state of the LocationProvider changes.
- Location:A data object containing location based information. Generally contains latitude and longitude, as well as a date and timestamp containing the time at which the location was determined. Might also contain elevation, speed, and heading information.
- Location based utility classes: A utility class for Geo-Coding which transforms addresses to GPS Coordinates and back, a data object class for addresses, and some classes for monitoring the status of the GPS satellites the device is locked onto.
In general, you usually query the LocationManager for the information you are seeking. You can check the current status of a LocationProvider, you can check the last known location reported by a LocationProvider, and you can register to receive updates from a LocationProvider all through the LocationManager (just to name a few). To receive updates on location information directly from a LocationProvider, you need to implement the LocationListener interface and register yourself with the LocationProvider.
In the subsections that follow, you add functionality to monitor and enable the GPS LocationProvider in the WalkAbout Activity class. You will record changes in location as the user's path. You will then implement an Overlay class that will draw this path onto the WalkAbout Activity's MapView object.
3.1 Testing & Enabling the GPS LocationProvider
It is entirely possible that the user has disabled the GPS hardware. Before you can monitor and record data from the GPS LocationProvider, the GPS hardware must be enabled. In this particular subsection, you will begin by querying the LocationManager for whether the GPS hardware is enabled on the device. If GPS is not enabled, you will allow the user to launch the Location Settings Activity to enable it from the "Enable GPS" Options MenuItem. After the user is done editing the Location Settings, they can return to the WalkAbout Activity by hitting the back button. When the GPS hardware is enabled the "Enable GPS" MenuItem will no longer be visible in the Options Menu.
The following diagram depicts two use cases (Click on the image to view it at full size). Both use cases have the application starting up without GPS hardware enabled and without the Network LocationProvider enabled. The first use case follows the dark green arrows along the top of the figure. The User clicks the menu button, then clicks the "Enable GPS" MenuItem. The Location Settings Activity launches, the user then decides not to enable the GPS. The user clicks the back button and is returned to the Walkabout Activity. The user clicks the Menu button, and the "Enable GPS" MenuItem is still visible. Notice that the Network and GPS Location Providers are disabled, that the MyLocationOverlay's Current Location Beacon is not present, and Compass heading location is inaccurate. This is because the MyLocation Overlay relies on the LocationProvider for displaying this information.
The second use case follows the golden arrows along the bottom of the figure. The User clicks the menu button, then clicks the "Enable GPS" MenuItem. The Location Settings Activity launches, the user then decides to enable the GPS. The user checks the "Enable GPS satellites" check box (Note that this option may be labeled different in different version of Android). The user clicks the back button and is returned to the Walkabout Activity. The user clicks the Menu button, and the "Enable GPS" MenuItem is no longer visible. Notice that when the user returns to the WalkAbout Activity after enabling the GPS hardware, that the MyLocationOverlay's current Location Beacon is now present, and Compass heading location is now accurate. It is also important to note that if the GPS Hardware is enabled when the application starts, then the "Enable GPS" MenuItem should not be visible.
3.1.1 Initialize LocationManager
In various parts of the WalkAbout class, you will request location information from the LocationManager attached to this application context. The WalkAbout class will store a reference to this LocationManager object in the m_locManager member variable for convenience. Initialize this member variable in the WalkAbout.initLocationData() method:
- Retrieve the LocationManager by calling the Activity.getSystemService(...) method, passing in the Context.LOCATION_SERVICE constant.
- Remember that this instance of the WalkAbout class is an Activity and therefore all public and protected Activity methods are members of this object.
- set m_locManager to the object returned to you. You will have to cast the return value, because getSystemService(...) returns an object of type Object.
3.1.2 Dynamically Update Options Menu
The Options menu should display the "Start/Stop" MenuItem as disabled if the GPS Location Provider is disabled. Additionally, the Options menu should NOT display the "Enable GPS" MenuItem at all if the GPS Location Provider is enabled.
Each time the Options menu is displayed, you should check to see if the GPS Location Provider is enabled and update the "Start/Stop" and "Enable GPS" MenuItems accordingly.
- You must do this from the Activity.onPrepareOptionsMenu(...). If this is done from the Activity.onCreateOptionsMenu(...) it will only be done the first time the menu is shown. See the Android Documentation on this method for more details.
- You can determine whether a Location Provider is enabled by calling the LocationManager.isProviderEnabled(...) method and passing in the name of the Provider. See the Android Documentation on this method for more details.
- The names of the different Providers are stored as string constants in the LocationManager class. Check the LocationManager Constants Documentation to determine which string to pass in.
You should be able to run your application and test that the "Start/Stop" and "Enable GPS" MenuItems are enabled and invisible respectively when the GPS Provider is enabled. They should also be disabled and visible respectively when the GPS Provider is disabled.
3.1.3 Implement "Enable GPS" MenuItem
When the GPS Location Provider is disabled, the user will be able to launch the settings activity to enable the GPS Location Provider from the "Enable GPS" Options MenuItem. Set and fill in the OnMenuItemClickListener for the "Enable GPS" MenuItem:
- Instantiate a new Intent object, passing in the Settings.ACTION_LOCATION_SOURCE_SETTINGS Action String constant.
- Call the Activity.startActivityForResult() method passing in the intent you just created and the WalkAbout.ENABLE_GPS_REQUEST_CODE static constant.
- We pass in the Request Code so that when we handle the result, we know which request generated the result. The result of any Activity that we launch gets processed by the same method and the Request Code mechanism lets us know which result is coming from which Activity.
3.1.4 Handle the Location Settings Activity Result
You should be able to run your application and test that you can launch the settings activity to enable the GPS provider from the "Enable GPS" MenuItem. When you hit the Back button you should return to the WalkAbout Activity. However, you should note that the MyLocationOverlay still won't be working.
After the user is done with the Location Settings Activity and returns to the WalkAbout Activity, you need to try re-enabling the MyLocationOverlay. Since you called the Activity.startActivityForResult(...) method to launch the Location Settings Activity, the WalkAbout Activity.onActivityResult(...) method will be called. The requestCode parameter in this method will contain the value that you passed into the startActivityForResult(...) method. You should test this requestCode parameter to see if it matches the WalkAbout.EBABLE_GPS_REQUEST_CODE constant. You test the requestCode because this same method will be called when any Activity that you launch returns a result (You must test this value because you will be launching another activity later and you want to be able to identify which Activity is returning a result).
Do this by overriding and filling in the Activity.onActivityResult(...) method:
- Make sure to call super.onActivityResult(...).
- Test the requestCode argument to ensure that it matches the request code that you used when you launched the CameraPreview activity.
- If the request codes don't match, then don't do anything and just return. If they do match then continue on.
- Attempt to re-enable the Compass and MyLocation beacon as you did in section 2.2.1.
You should now run your application with all Location Providers disabled and test that the MyLocation Overlay is displayed properly once you enable the GPS LocationProvider.
3.2 Retrieving GPS Location Information
In this subsection, you will implement the functionality necessary to monitor and record GPS location information. You will monitor GPS location information by making the WalkAbout Activity register itself with the GPS LocationProvider. By doing this, the WalkAbout Activity will be notified by the GPS LocationProvider when the location changes. However, before the WalkAbout Activity class can register itself with the GPS LocationProvider, it must implement the com.google.android.maps.LocationListener interface. You will start by making the WalkAbout Activity implement the LocationListener interface.
You will then implement a "Start/Stop" MenuItem that will toggle the WalkAbout Activity between actively-recording and recording-stopped states. While in the actively-recording state, the WalkAbout Activity will be registered to receive updates from the GPS LocationProvider. As the WalkAbout Activity receives notices about location changes, it will re-center its MapView about the new location. It will also store changes in location as latitude-longitude coordinates in an ArrayList. Additionally, while in the actively-recording state the "Start/Stop" MenuItem will display the word "Stop" to indicate that clicking on the MenuItem will cause the WalkAbout Activity to switch to the recording-stopped state.
While in the recording-stopped state, the WalkAbout Activity will no longer be registered to receive updates from the GPS LocationProvider. Additionally, while in the actively-recording state the "Start/Stop" MenuItem will display the word "Start" to indicate that clicking on the MenuItem will cause the WalkAbout Activity to switch to the recording-stopped state. Note that when the Application starts up, it is by default in the recording-stopped state. When the user switches from the recording-stopped state to the actively-recording state, the ArrayList used to record changes in location should be cleared. See the Figure below for more details on the use case:
3.2.1 Implement LocationListener Interface
The WalkAbout Activity class will receive updates from the GPS Location Provider by registering itself with the Provider. In order to register with the Provider, the WalkAbout Activity must implement the LocationListener interface. This interface provides a set of callback methods that the Provider will call when the location changes, the Provider is enabled, the Provider is disabled, or when the status of the Provider changes.
In particular, you will monitor and record location changes, and stop recording in the event that the Provider is disabled for some reason. The WalkAbout class records location changes in an ArrayList<GeoPoint> member variable named m_arrPathPoints. It also maintains the current recording state (whether it is currently recording or not) in a boolean member variable named m_bRecording. Perform the following initializations in the initLocationData() method.
- Initialize m_arrPathPoints to a new ArrayList<GeoPoint>.
- Initialize m_bRecording to false.
Make the WalkAbout MapActivity class implement the LocationListener interface:
- There is a total of four methods that you must implement. See the Android Documentation on LocationListener for a complete listing and description of each.
- Of the four methods, you only need to fill in two of them, onLocationChanged(...) and onProviderDisabled(...).
- You still need to declare the other two methods but you can leave them as empty stubs.
- Record location changes in the onLocationChanged(...) method.
- The Location argument passed in has both degrees latitude and longitude values, represented as type double. Some of the Google Maps classes that you will be using later on make use of the GeoPoint class, which represents latitude and longitude as int values of microdegrees. This is why you are storing an ArrayList of GeoPoints and not Locations. You will have to convert from Location to GeoPoint.
- 1 degree is equal to 1E6 microdegrees. The WalkAbout Activity class has a constant for this value named GEO_CONVERSION
- 99.123456 degrees = 99123456 microdegrees
- Get the degrees latitude value from the Location argument by calling the Location.getLatitude() method and convert it to microdegrees.
- Convert by multiplying the returned double by the WalkAbout.GEO_CONVERSION integer constant.
- Get the degrees longitude value from the Location argument by calling the Location.getLongitude() method and convert it to microdegrees.
- Instantiate a new GeoPoint object, passing into the constructor the latitude and longitude values you just converted into microdegrees.
- You will have to cast these to type int as they are currently of type double.
- Add the GeoPoint to m_arrPathPoints.
- In the onProviderDisabled(...) method, instruct the WalkAbout Activity to stop recording location changes.
- Do this by making a call to WalkAbout.setRecordingState(false). You will implement this method in the sections that follow.
3.2.2 Dynamically Update the "Start/Stop" MenuItem Text
When the Options Menu is displayed, the text of the "Start/Stop" MenuItem should be set to the R.string.startRecording if the activity is not recording and R.string.stopRecording if the activity is recording.
3.2.3 Implement "Start/Stop" MenuItem
When the "Start/Stop" MenuItem is clicked, the recording state should be toggled. If the activity is currently recording, then the activity should stop recording. Conversely, if the activity is not recording, then the activity should start recording.
The functionality for setting the recording state will be encapsulated in the WalkAbout.setRecordingState(boolean recording) method. Passing in a value of true indicates that the activity should start recording and passing in a value of false indicates that the activity should stop recording.
Set and fill in the "Start/Stop" OnMenuItemClickListener so that it toggles the recording state by calling WalkAbout.setRecordingState(...) with the proper value.
- This is all this method should do.
- Make use of the m_bRecordingState member variable to determine the current recording state.
Fill in the setRecordingState(...) method:
- You should update the m_bRecording member variable with the new state.
- If the new state is not recording you should tell the GPS Provider to stop sending you Location Updates.
- You can do this by calling the LocationManager.removeUpdates(...) method and pass in the LocationListener that you want to un-register.
- Otherwise, if the new state is recording, you should re-initialize your recording data.
- All previously recorded GeoPoints should be erased. Do not instantiate a new list of GeoPoints, just clear your current one.
- You should get used to trying to re-use objects when possible. It is expensive to create new ones and it may be some time before the old ones get garbage collected.
- Initialize your list of GeoPoints with the last known location from the GPS Provider.
- You can do this by calling the LocationManager.getLastKnownLocation(...) method.
- You can then manually call your WalkAbout.onLocationChanged(...) method with the Location you just retrieved. This will update your list of GeoPoints for you.
- Tell the GPS Provider to start sending you Location Updates.
- You can do this by calling the LocationManager.requestLocationUpdates(...) method.
- Pass in the String name of the Location Provider you want to receive Location Updates from
- Pass in WalkAbout.MIN_TIME_CHANGE, to specifiy the minimum amount of time between updates.
- Pass in WalkAbout.MIN_DISTANCE_CHANGE, to specifiy the minimum amount of distance between updates.
- Pass in the LocationListener which should receive the updates.
If you run your application, it should now record GPS Location changes. You can test this by adding a Toast notification to the WalkAbout.onLocationChanged(...) method that prints out the new location. Don't forget to remove the Toast after you've finished testing.
If you are running this on the emulator, you can simulate location changes on the emulator two different ways. You can do this from the DDMS perspective in Eclipse, or from the Console. For instructions on how to open up a console, see the Android reference on Using the Console
- DDMS: see the DDMS documentation on Emulator Controls. Feel free to use the testGPS.gpx file included in the root folder of the skeleton project as a test path.
- Console: see the documenation on Location Services
3.2.4 Center MapView on Current Location
As the application stands now, when you change location, the MapView does not move. It is therefore possible for your MyLocation beacon to move off of the MapView. You will now change this so that when your position changes, the MapView will center itself about the MyLocation beacon. This is done using the MapView's MapController object. The MapController can be used for a number of different things including changing the zoom level.
- In the WalkAbout.onLocationChanged(...) method, retrieve the MapController from your MapView by calling the MapView.getController() method.
- Use the MapController.animateTo(Point point) method to force the map to scroll so that the current location is now in the center.
You should now be able to run your application to ensure that the Map is always centered about the device's current location.
3.3 Path Overlay
For this subsection, you will implement your own Overlay class that draws a path on top of a MapView. You will begin by filling in the PathOverlay class so that it draws a green starting circle at the first point in the path, and then draws a red line to connect each subsequent point in the path. The PathOverlay class will contain a reference to a List of GeoPoints. You will then add the PathOverlay to the WalkAbout Activity's MapView object, setting it up so that the PathOverlay uses the WalkAbout Activity's recorded list of GeoPoints as the path that it should draw.
3.3.1 Fill in the PathOverlay Class
Begin by filling in the constructor:
- Set m_arrPathPoints to the pathPoints constructor argument. This is the list of GeoPoints that represents the path that this overlay class will draw. Notice that it is declared as final; from the perspective of the PathOverlay class this object is immutable/Read-Only. You should not attempt to modify the contents of this List, you should only read from it.
- Initialize each of the other four member variables with new default constructed objects. You will use these objects to help you draw the path. Instead of recreating them every time the draw method is called, you will simply re-use them to improve performance.
The only thing that this object is supposed to do is draw the path overlay, and thus it has only one method, draw(...). The PathOverlay should draw the first point as a green circle and connect each subsequent point with red a line. If the list of GeoPoints is empty or null, the method should do nothing.
The Canvas argument that is passed into the draw(...) method is what performs the drawing for you. You can get something to show up on the screen by asking the Canvas object to draw something for you. You do that by telling it what to draw, where to draw it, and what color it should use. The MapView argument is a reference to the Map on which this overlay will be drawn. Don't worry about the shadow argument for now.
Start by drawing the starting circle:
- Set the color to green by calling the Paint.setARGB(...) method on m_paint, passing in the Red, Green, Blue, and Alpha values.
- Specify where to draw the circle by calling the RectF.set(...) method on m_rect. This outlines a Rectangle in which to draw the circle. The RectF.set(...) method takes in the x values of the left & right edges and the y values of the top & bottom edges.
- Before you can call the RectF.set(...) method, you need to translate your first GeoPoint coordinate into screen coordinates.
- Call the MapView.getProjection() method to retrieve a projection object that will perform the translation for you.
- You can now call the Projection.toPixels(...) method, passing in your first GeoPoint and the m_point member variable.
- After the call, m_point will contain the x-y screen coordinates.
- Use the following line to set the bounding rectangle:
m_rect.set(m_point.x-START_RADIUS, m_point.y-START_RADIUS, m_point.x+START_RADIUS, m_point.y+START_RADIUS);
- Tell the canvas to draw the circle by calling Canvas.drawOval(...), passing in your bounding rectangle and the color it should use.
You should now start drawing your path. For each point in the list of GeoPoints, draw a red line starting from the current point to the point following it. For example, if your list has 3 points in it, you should draw a line connecting point-1 to point-2 then you should draw a line connecting point-2 to point-3.
- Use m_point2 to store the second set of screen coordinates.
- The line should have a thickness equal to PathOverlay.PATH_WIDTH. You can set the width of the line by calling the Paint.setStrokeWidth(...) method.
- You can use the Canvas.drawLine(...) method to draw the line.
- Note: You won't use the m_rect to draw the path, you only use it to draw the starting circle.
3.3.2 Add a PathOverlay to Your Map
In the WalkAbout activity class
- Initialize and add a PathOverlay to your MapView in the WalkAbout.initLayout() method.
- Since the PathOverlay only gets redrawn when the MapView gets redrawn, you need to notify the MapView to redraw itself any time you update your path/recorded-list-of-GeoPoints.
- You tell the MapView to redraw itself by calling MapView.postInvalidate();
- When you add GeoPoints to your recording-list/path, you need to redraw the MapView.
- When you clear your recording-list/path, you need to redraw the MapView
You should now be able to run your application to ensure that when recording, your path gets continuously updated and displayed on the MapView.
4. Using The Camera
4.1 Camera Preview
Android allows applications direct access to the Camera Hardware. In the section that follows you will implement the WalkAbout Activity "Take Picture" MenuItem. The MenuItem should only be enabled while the Application is in the actively-recording state. When the MenuItem is clicked, it will launch the CameraPreview Activity. It is your job to extend this Activity so that it can capture a picture and save it to the SD-Card. The CameraPreview Activity was borrowed from the Google API Demos application, and currently is only able to display a full screen preview of what the camera is looking at.
The CameraPreview Activity will take a picture once the user touches anywhere on the screen. Once the picture is taken, it will be saved on the SD-Card and the file-name will be returned to the WalkAbout Activity as a result. When the user returns to the WalkAbout Activity, a short Toast notification containing a success message and the file-name the picture was saved to will be displayed to the user. Alternatively, the user can hit the back button without taking a picture to return to the WalkAbout Activity, effectively canceling the CameraPreview Activity. Should the CameraPreview Activity be canceled, the user will see a short Toast notification containing a failure message.
4.1.1 Dynamically Update Options Menu
When the WalkAbout Options Menu is displayed, the "Take Picture" MenuItem should only be enabled if the Activity is recording GPS location changes. If the Activity is not recording, the MenuItem should be disabled.
4.1.2 Launch the CameraPreview Activity
When the "Take Picture" MenuItem is clicked, the CameraPreview Activity should be launched. Currently, the CameraPreview Activity class does not have the ability to take a picture, but rather only provides a preview image. You will add the functionality to this class to take the picture in the next section. Before you can do that, you need to update your the AndroidManifest.xml:
In the WalkAbout Activity, set and fill in the "Take Picture" OnMenuItemClickListener so that it launches the CameraPreview Activity and recieves a result back from the activity.
- Create a new Intent, passing in a reference to this application Context and the edu.calpoly.android.walkabout.CameraPreview Class object.
- Call Activity.startActivityForResult(...), passing in the Intent object and the WalkAbout.PICTURE_REQUEST_CODE integer constant.
4.1.3 Take a Picture from the CameraPreview Activity
The CameraPreview Activity class comes from the Google Android API Demos application which provides sample applications for a number of different Android API's. In order to display a Camera Preview you need to use some advanced Android features. Instead of having you implement this yourself, it's provided for you so that you can see how its done.
- Notice the inner Preview class that extends the SurfaceView class. This is the View that renders the Camera Preview Image and displays it to the user.
- The SurfaceView class is used when rendering is too computationally expensive to be done from the GUI thread or the View needs to be refreshed very quickly.
- The Preview class uses a Camera object and its Camera.Parameters inner class to retrieve the camera image data and manipulate its settings.
A single getter method was added to the Preview class for the purposes of retrieving a reference to the Camera object. This was done so that you can add functionality to the CameraPreview Activity class to take a picture. When a user clicks anywhere on the CameraPreview's mPreview object (which occupies the entire screen) the Preview object's camera should take a picture.
- Set an OnClickListener for the CameraPreview's Preview object and fill in its onClick(...) method
- Retrieve the Preview object's Camera and call its takePicture(...) method.
- This method takes 3 separate listeners, each of which implement a different callback that will be called during different stages of the photo taking process. All of these can be null if we don't want to respond to any of these stages.
- The First is a ShutterCallback that will be called once the image has been captured.
- The Second is a PictureCallback that will be called once the raw image data has been generated.
- The Third is another PictureCallback that will be called once the jpeg data has been generated. This is the only one we are interested in.
- Pass in null for the first two parameters and pass in a reference to this CameraPreview instance for the third. CameraPreview implements the PictureCallback interface. This will make CameraPreview's onPictureTaken(...) method be called once the jpeg data has been generated.
You should be able to run your application and verify that you can launch the CameraPreview activity. You should be able to see a preview of what the camera is seeing. If you add a Toast notification to the CameraPreview.onPictureTaken(...) method, you should see the notification when you click anywhere on the screen.
4.1.4 Save the Picture to SD-Card
Now that the picture has been taken, we need to save the jpeg data into a file on the SD-Card when the CameraPreview.onPictureTake(...) method is called. In order for your application to write data to the SD-Card, it must declare that it uses the WRITE_EXTERNAL_STORAGE permission in its manifest.
Now you can implement the File-Output functionality by filling in the CameraPreview.onPictureTaken(...) method:
- Instantiate a new java.io.File object, passing in the path to the SD-Card directory and the name of the File you want to create.
- You can retrieve the path to the SD-Card directory by calling the static Environment.getExternalStorageDirectory() method.
- To guarantee that all of your file names are unique, use the Current System Time appended with the ".jpg" extension as your file name.
- For example: If you call System.currentTimeMillis() your file will look something like "1111111111111.jpg"
- Instantiate a new java.io.FileOutputStream object passing your new File object into the constructor.
- Write the byte data argument passed into the onPictureTaken(...) method out to the FileOutputStream.
- Flush the stream and close it.
- You must catch any IOExceptions that occur.
- Should an IOException occur, set the result of the Activity by calling the Activity.setResult(...) method.
- Pass in Activity.RESULT_CANCELED as the result code and null as the Intent data to indicate that the Picture was not taken successfully.
- call the Activity.finish() method
- If the file was written successfully:
- Default instantiate a new Intent object. You can use this Intent object to pass the path to the image file back to the WalkAbout Activity.
- Use the Intent.putExtra(...) method to store the absolute path to the image file. You can use CameraPreview.IMAGE_FILE_NAME as the key.
- Set the result for the Activity with Activity.RESULT_OK and the Intent data, then finish the Activity.
After the CameraPreview Activity finishes, the WalkAbout Activity needs to handle the result of the Intent. The WalkAbout Activity should make a Toast Notification displaying either success or failure as returned by the CameraPreview Activity. If the pictures was taken successfully, the path to the created image file should be displayed in the notification. Do this by overriding and filling in the Activity.onActivityResult(...) method:
- Make sure to call super.onActivityResult(...).
- Test the requestCode argument to ensure that it matches the request code that you used when you launched the CameraPreview activity.
- If the request codes don't match, then don't do anything and just return. If they do match then continue on.
- Test the resultCode argument.
- If it equals Activity.RESULT_CANCELED then print a Toast Notification displaying the R.string.pictureFail string resource.
- If it equals Activity.RESULT_OK then print a Toast Notification displaying the R.string.pictureSuccess string resource and the full image file path.
- You can retrieve the image file path by calling getExtras().getString(CameraPreview.IMAGE_FILE_NAME) on the Intent data argument.
You should be able to run your application and verify that the Toast Notification displays the proper text when the CameraPreview Activity finishes.
5. Private Application Files
Android allows you to Create, Write, Read and Delete local files. These files become private to the application that created them by default. You have the option of overriding this privacy mechanism, allowing them to be shared with other applications.
5.1 Creating, Writing, & Deleting Files
You will now implement WalkAbout Activity's "Save" MenuItem. This MenuItem's OnMenuItemClickListener should only make a single call to the WalkAbout.saveRecording() method. The functionality necessary to save the current recorded path should be composed in the WalkAbout.saveRecording() method. You should fill in the saveRecording method so that it properly writes out only the contents of m_arrPathPoints to a private application file. You should only ever create one file and it should be truncated/cleared each time you write to it.
The format of the file is quite simple. The contents of m_arrPathPoints will be written into a single continuous line. The latitude and longitude of each GeoPoint in m_arrPathPoints should be written out in that order, separated by a comma, and no spaces between them. Each GeoPoint should be separated by a semi-colon, and no spaces between them. So for example:
m_arrPathPoints = [(lat1,long1),(lat2,long2),(lat3,long3)]
The file should look like:
If the Save was performed successfully, then a Toast notification should be displayed containing the R.string.saveSuccess resource string. If an exception is thrown you should display a Toast notification containing the R.string.saveFailed resource string. If there is no data to save, as is the case on the initial loading of the application, then a Toast notification should be displayed containing the R.string.saveNoData resource string. You are tasked to do this on your own, however, you should make use of the following hints:
5.2 Reading Files
Your last task is to implement the WalkAbout Activity's "Load" MenuItem. This MenuItem's OnMenuItemClickListener should only set the recording state to false and make a call to the WalkAbout.loadRecording() method. The rest of the File Loading functionality should be implemented in the loadRecording() method. You should fill in the loadRecording method so that it properly initializes m_arrPathPoints to contain only the data in the file that saveRecording() writes out. Once finished, the path loaded from the file should be displayed exactly as it was when it was first recorded.
If the Load was performed successfully, then a Toast notification should be displayed containing the R.string.loadSuccess resource string. If an exception is thrown, you should display a Toast notification containing the R.string.loadFailed resource string. You are to do this task on your own, however, you should make use of the following hints:
To complete this lab you will be required to:
Put your entire project directory into a .zip or .tar file, similar to the stub you were given. Submit the archive using handin on a Cal Poly Unix server. The name of your archive should be lab5<cal-poly-username>.zip|tar. So if your username is jsmith and you created a zip file, then your file would be named lab5jsmith.zip. You would submit this file with the command handin djanzen 409Lab5 lab5jsmith.zip.
In addition, bring your app running on your phone to lab on the due date. You must demo it to Dr. Janzen for full credit.
Complete the following survey for Lab 5:
Primary Author: James Reed