Creating Map-Based Activities
❘
267
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setAltitudeRequired(false);
criteria.setBearingRequired(false);
criteria.setCostAllowed(true);
criteria.setPowerRequirement(Criteria.POWER_LOW);
String provider = locationManager.getBestProvider(criteria, true);
Location location =
locationManager.getLastKnownLocation(provider);
updateWithNewLocation(location);
locationManager.requestLocationUpdates(provider, 2000, 10,
locationListener);
}
6. The final step is to modify the
updateWithNewLocation
method to re-center the map on the
current location using the Map Controller.
private void updateWithNewLocation(Location location) {
String latLongString;
TextView myLocationText;
myLocationText = (TextView)findViewById(R.id.myLocationText);
String addressString = "No address found";
if (location != null) {
// Update the map location.
Double geoLat = location.getLatitude()*1E6;
Double geoLng = location.getLongitude()*1E6;
GeoPoint point = new GeoPoint(geoLat.intValue(),
geoLng.intValue());
mapController.animateTo(point);
Overlays enable you to add annotations and click handling to
MapViews
. Each Overlay lets you draw
2D primitives, including text, lines, images, and shapes, directly onto a canvas, which is then overlaid
ontoaMapView.
You can add several Overlays onto a single map. All the Overlays assigned to a Map View are added
as layers, with newer layers potentially obscuring older ones. User clicks are passed through the stack
until they are either handled by an Overlay or registered as clicks on the Map View itself.
Creating New Overlays
Each Overlay is a canvas with a transparent background that is layered onto a Map View and used to
handle map touch events.
To add a new Overlay create a new class that extends
Overlay
. Override the
draw
method to draw the
annotations you want to add, and override
onTap
to react to user clicks (generally made when the user
taps an annotation added by this Overlay).
Listing 8-8 shows the framework for creating a new Overlay that can draw annotations and handle
user clicks.
LISTING 8-8: Creating a new Overlay
import android.graphics.Canvas;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
public class MyOverlay extends Overlay {
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow == false) {
Projection
class lets you translate between latitude/longitude coordinates (stored as
GeoPoints
)
and x/y screen pixel coordinates (stored as
Points
).
A map’s Projection may change between subsequentcalls to draw, so it’s good practice to get a new
instance each time. Get a Map View’s Projection by calling
getProjection
.
Projection projection = mapView.getProjection();
Use the
fromPixel
and
toPixel
methods to translate from GeoPoints to Points and vice versa.
For performance reasons, you can best use the
toPixel
Projection method by passing a Point object to
be populated (rather than relying on the return value), as shown in Listing 8-9.
LISTING 8-9: Using map projections
Point myPoint = new Point();
// To screen coordinates
projection.toPixels(geoPoint, myPoint);
// To GeoPoint location coordinates
projection.fromPixels(myPoint.x, myPoint.y);
Drawing on the Overlay Canvas
You handle Canvas drawing for Overlays by overriding the Overlay’s
draw
RectF oval = new RectF(myPoint.x-rad, myPoint.y-rad,
myPoint.x+rad, myPoint.y+rad);
// Draw on the canvas
canvas.drawOval(oval, paint);
canvas.drawText("Red Circle", myPoint.x+rad, myPoint.y, paint);
}
}
For more advanced drawing features see Chapter 11, where gradients, strokes, and
filters are introduced.
Handling Map Tap Events
To handle map taps (user clicks), override the
onTap
event handler within the Overlay extension class.
The
onTap
handler receives two parameters:
➤ A
GeoPoint
that contains the latitude/longitude of the map location tapped
➤ The
MapView
that was tapped to trigger this event
When you are overriding
onTap
, the method should return
true
if it has handled a particular tap and
false
to let another Overlay handle it, as shown in Listing 8-11.
LISTING 8-11: Handling map-tap events
the list safely. You should still iterate over the list within a synchronization block synchronized on the
List.
To add an Overlay onto a Map View, create a new instance of the Overlay and add it to the list, as
shown in the following snippet.
List<Overlay> overlays = mapView.getOverlays();
MyOverlay myOverlay = new MyOverlay();
overlays.add(myOverlay);
mapView.postInvalidate();
The added Overlay will be displayed the next time the Map View is redrawn, so it’s usually a good
practice to call
postInvalidate
after you modify the list to update the changes on the map display.
Annotating ‘Where Am I?’
This final modification to ‘‘Where Am I?’’ creates and adds a new Overlay that displays a white circle
at the device’s current position.
1. Start by creating a new
MyPositionOverlay
Overlay class in the Where Am I? project.
package com.paad.whereami;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.location.Location;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
public class MyPositionOverlay extends Overlay {
@Override
GeoPoint geoPoint;
geoPoint = new
GeoPoint(latitude.intValue(),longitude.intValue());
// Convert the location to screen pixels
Point point = new Point();
projection.toPixels(geoPoint, point);
RectF oval = new RectF(point.x - mRadius, point.y - mRadius,
point.x + mRadius, point.y + mRadius);
// Setup the paint
Paint paint = new Paint();
paint.setARGB(250, 255, 255, 255);
paint.setAntiAlias(true);
paint.setFakeBoldText(true);
Paint backPaint = new Paint();
backPaint.setARGB(175, 50, 50, 50);
backPaint.setAntiAlias(true);
RectF backRect = new RectF(point.x + 2 + mRadius,
point.y - 3*mRadius,
point.x + 65, point.y + mRadius);
// Draw the marker
canvas.drawOval(oval, paint);
canvas.drawRoundRect(backRect, 5, 5, backPaint);
canvas.drawText("Here I Am",
Creating Map-Based Activities
❘
273
point.x + 2*mRadius, point.y,
paint);
}
super.draw(canvas, mapView, shadow);
LocationManager locationManager;
String context = Context.LOCATION_SERVICE;
locationManager = (LocationManager)getSystemService(context);
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setAltitudeRequired(false);
criteria.setBearingRequired(false);
criteria.setCostAllowed(true);
criteria.setPowerRequirement(Criteria.POWER_LOW);
String provider = locationManager.getBestProvider(criteria, true);
Location location = locationManager.getLastKnownLocation(provider);
updateWithNewLocation(location);
locationManager.requestLocationUpdates(provider, 2000, 10,
locationListener);
}
5. Finally, update the
updateWithNewLocation
method to pass the new location to the Overlay.
private void updateWithNewLocation(Location location) {
String latLongString;
TextView myLocationText;
274
❘
CHAPTER 8 MAPS, GEOCODING, AND LOCATION-BASED SERVICES
myLocationText = (TextView)findViewById(R.id.myLocationText);
String addressString = "No address found";
if (location != null) {
// Update my location marker
positionOverlay.setLocation(location);
// Update the map location.
}
All code snippets in this example are part of the Chapter 8 Where Am I? project, available for download at Wrox.com.
When run, your application will display your current device location with a white circle and supporting
text, as shown in Figure 8-6.
Creating Map-Based Activities
❘
275
It’s worth noting that this is not the preferred technique for displaying your current
location on a map. This functionality is implemented natively by Android through
the
MyLocationOverlay
class. If you want to display and follow your current
location, you should consider using (or extending) this class, as shown in the next
section, instead of implementing it manually as shown here.
Introducing My Location Overlay
FIGURE 8-6
The
MyLocationOverlay
class is a special Overlay designed
to show your current location and orientation on a
MapView
.
To use My Location Overlay you need to create a new
instance, passing in the application Context and target Map
View,andaddittothe
MapView
’s Overlay list, as shown
here:
List<Overlay> overlays =
mapView.getOverlays();
CHAPTER 8 MAPS, GEOCODING, AND LOCATION-BASED SERVICES
To add an
ItemizedOverlay
marker layer to your map, start by creating a new class that extends
ItemizedOverlay<OverlayItem>
, as shown in Listing 8-12.
ItemizedOverlay
is a generic class that lets you create extensions based on any
OverlayItem
-derived subclass.
Within the constructor you need to call through to the superclass after defining the bounds for your
default marker. You must then call
populate
to trigger the creation of each
OverlayItem
;
populate
must be called whenever the data used to create the items changes.
Within the implementation, override
size
to return the number of markers to display and
createItem
to create a new item based on the index of each marker.
LISTING 8-12: Creating a new Itemized Overlay
import android.graphics.drawable.Drawable;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.OverlayItem;
public class MyItemizedOverlay extends ItemizedOverlay<OverlayItem> {
public MyItemizedOverlay(Drawable defaultMarker) {
MyItemizedOverlay markers = new
MyItemizedOverlay(r.getDrawable(R.drawable.marker));
overlays.add(markers);
Note that the map markers placed by the Itemized Overlay use state to indicate if
they are selected. Use the
StateListDrawable
described in Chapter 4 to indicate
when a marker has been selected.
In Listing 8-12, the list of Overlay items is static and defined in code. More typically your Overlay items
will be a dynamic ArrayList to which you will want to add and remove items at run time.
Listing 8-13 shows the skeleton class for a dynamic Itemized Overlay implementation, backed by an
ArrayList, and supporting the addition and removal of items at run time.
LISTING 8-13: Skeleton code for a dynamic Itemized Overlay
public class MyDynamicItemizedOverlay extends ItemizedOverlay<OverlayItem>
{
private ArrayList<OverlayItem> items;
public MyDynamicItemizedOverlay(Drawable defaultMarker) {
super(boundCenterBottom(defaultMarker));
items = new ArrayList<OverlayItem>();
populate();
}
public void addNewItem(GeoPoint location, String markerText,
String snippet) {
items.add(new OverlayItem(location, markerText, snippet));
populate();
}
public void removeItem(int index) {
items.remove(index);
populate();
}
containing it. Pass in the View you want to pin and the
layout parameters to use.
The
MapView.LayoutParams
parameters you pass in to
addView
determine how, and where, the View is
added to the map.
To add a new View to the map relative to the screen, specify a new
MapView.LayoutParams,
including
arguments that set the height and width of the View, the x/y screen coordinates to pin to, and the
alignment to use for positioning, as shown in Listing 8-14.
LISTING 8-14: Pinning a View to a map
int y = 10;
int x = 10;
EditText editText1 = new EditText(getApplicationContext());
editText1.setText("Screen Pinned");
MapView.LayoutParams screenLP;
screenLP = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT,
MapView.LayoutParams.WRAP_CONTENT,
x, y,
MapView.LayoutParams.TOP_LEFT);
mapView.addView(editText1, screenLP);
To pin a View relative to a physical map location, pass four parameters when constructing the new
Map View
LayoutParams
, representing the height, width, GeoPoint to pin to, and layout alignment as
shown in Listing 8-15.
LISTING 8-15: Pinning a View to a geographical location
1. Create a new earthquake_map.xml layout resource that includes a
MapView
, being sure to
include an
android:id
attribute and an
android:apiKey
attribute that contains your Android
Maps API key.
<?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="fill_parent">
<com.google.android.maps.MapView
android:id="@+id/map_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:enabled="true"
android:clickable="true"
android:apiKey="myapikey"
/>
</LinearLayout>
2. Create a new
EarthquakeMap
Activity that inherits from
MapActivity
.Use
setContentView
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=".Preferences"
android:label="Earthquake Preferences"/>
<activity android:name=".EarthquakeMap"
android:label="View Earthquakes"/>
<provider android:name=".EarthquakeProvider"
android:authorities="com.paad.provider.earthquake" />
<uses-library android:name="com.google.android.maps"/>
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
4. Add a new menu option to the
Earthquake
Activity to display the
EarthquakeMap
Activity.
4.1. Start by adding a new string to the strings.xml resource for the menu text.
<?xml version="1.0" encoding="autf-8"?>
<resources>
<string name="app_name">Earthquake</string>
<string name="quake_feed">
http://earthquake.usgs.gov/eqcenter/catalogs/1day-M2.5.xml
</string>
<string name="menu_update">Refresh Earthquakes</string>
<string name="auto_update_prompt">Auto Update?</string>
5. Now create a new
EarthquakeOverlay
class that extends
Overlay
. It will draw the position
and magnitude of each earthquake on the Map View.
package com.paad.earthquake;
import java.util.ArrayList;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
public class EarthquakeOverlay extends Overlay {
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
Projection projection = mapView.getProjection();
if (shadow == false) {
// TODO: Draw earthquakes
}
}
}
5.1. Add a new constructor that accepts a
Cursor
to the current earthquake data, and
refreshQuakeLocations
from the Overlay’s constructor. Also register a
DataSetObserver
on the results Cursor that refreshes the Earthquake Location list
if a change in the Earthquake Cursor is detected.
public EarthquakeOverlay(Cursor cursor) {
super();
earthquakes = cursor;
quakeLocations = new ArrayList<GeoPoint>();
refreshQuakeLocations();
earthquakes.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
refreshQuakeLocations();
}
});
}
5.4. Complete the
EarthquakeOverlay
by overriding the
draw
method to iterate over the
list of
GeoPoints
, drawing a marker at each earthquake location. In this example
a simple red circle is drawn, but you could easily modify it to include additional
information, such as by adjusting the size of each circle based on the magnitude of
the quake.
int rad = 5;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.earthquake_map);
String earthquakeURI = EarthquakeProvider.CONTENT_URI;
earthquakeCursor = getContentResolver().query(earthquakeURI,
null, null, null,
null);
MapView earthquakeMap = (MapView)findViewById(R.id.map_view);
EarthquakeOverlay eo = new EarthquakeOverlay(earthquakeCursor);
earthquakeMap.getOverlays().add(eo);
}
7. Finally, override
onResume
to call
requery
on the Earthquake result set whenever this Activ-
ity becomes visible. Also, override
onPause
and
onDestroy
to optimize use of the Cursor
resources.
@Override
public void onResume() {
earthquakeCursor.requery();
super.onResume();
}
@Override
public void onPause() {
earthquakeCursor.deactivate();
and Views
(including View Groups and layouts).
In Chapter 9 you’ll learn how to work from the background.
You’ll be introduced to the Service component and learn
how to move processing onto background threads. To inter-
act with the user while hidden from view, you’ll use Toasts
to display transient messages and the Notification Manager
to ring, vibrate, and flash the phone.
9
Working in the Background
WHAT’S IN THIS CHAPTER?
➤ Creating, starting, and stopping Services
➤ Binding Services to Activities
➤ Setting Service priority to foreground
➤ Using AsyncTasks to manage background processing
➤ Creating background threads and using Handlers to synchronize with
the GUI thread
➤ Displaying Toasts
➤ Using the Notification Manager to notify users of application events
➤ Creating insistent and ongoing Notifications
➤ Using Alarms to schedule application events
Android offers the
Service
class to create application components specifically to handle opera-
tions and functionality that should run invisibly, without a user interface.
Android accords Services a higher priority than inactive Activities, so they’re less likely to be
killed when the system requires resources. In fact, should the run time prematurely terminate a
Service that’s been started, it can be configured to restart as soon as sufficient resources become
available. In extreme cases, the termination of a Service — such as an interruption in music
playback — will noticeably affect the user experience, and in these cases a Service’s priority can
Unlike Activities, which present a rich graphical interface to users, Services run in the background —
updating your Content Providers, firing Intents, and triggering Notifications. They are the perfect
means of performing ongoing or regular processing and of handling events even when your applica-
tion’s Activities are invisible or inactive, or have been closed.
Services are started, stopped, and controlled from other application components, including other
Services, Activities, and Broadcast Receivers. If your application performs actions that don’t depend
directly on user input, Services may be the answer.
Started Services always have higher priority than inactive or invisible Activities, making them less likely
to be terminated by the run time’s resource management. The only reason Android will stop a Service
prematurely is to provide additional resources for a foreground component (usually an Activity). When
that happens, your Service will be restarted automatically when resources become available.
If your Service is interacting directly with the user (for example, by playing music) it may be necessary to
increase its priority to that of a foreground Activity. This will ensure that your Service isn’t terminated
except in extreme circumstances, but reduces the run time’s ability to manage its resources, potentially
degrading the overall user experience.
Applications that update regularly but only rarely or intermittently need user interaction are good
candidates for implementation as Services. MP3 players and sports-score monitors are examples of
applications that should continue to run and update without a visible Activity.
Further examples can be found within the software stack itself: Android implements several Services,
including the Location Manager, Media Controller, and Notification Manager.
Introducing Services
❘
287
Creating and Controlling Services
In the following sections you’ll learn how to create a new Service, and how to start and stop it using
Intents and the
startService
method. Later you’ll learn how to bind a Service to an Activity to provide
a richer communications interface.
Creating a Service
onStartCommand
handler replaces the
onStart
event that was used prior to Android 2.0. By con-
trast, it enables you to tell the system how to handle restarts if the Service is killed by the system prior
to an explicit call to
stopService
or
stopSelf
.
The following snippet extends Listing 9-1 to show the skeleton code for overriding the
onStartCommand
handler. Note that it returns a value that controls how the system will respond if the Service is restarted
after being killed by the run time.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Launch a background thread to do processing.
return Service.START_STICKY;
}
Services are launched on the main Application thread, meaning that any processing done in the
onStartCommand
handler will happen on the main GUI thread. The standard pattern for implementing
288
❘
CHAPTER 9 WORKING IN THE BACKGROUND
a Service is to create and run a new thread from
onStartCommand
to perform the processing in the
background and stop the Service when it’s complete (you will be shown how to create and manage
background threads later in this chapter).
stopSelf
to terminate once that command has been
completed.
Following termination by the run time, Services set to this mode will restart only if there are
pending start calls. If no
startService
calls have been made since the Service was terminated,
the Service will be stopped without a call being made to
onStartCommand
.
This mode is ideal for Services that handle specific requests, particularly regular processing
such as updates or network polling. Rather than restarting the Service during a period of
resource contention, it’s often more prudent tolet the Service stop and retry at the next sched-
uled interval.
➤
START_REDELIVER_INTENT
In some circumstances you will want to ensure that the com-
mands you have requested from your Service are completed.
This mode is a combination of the first two — if the Service is terminated by the run time, it
will restart only if there are pending start calls or the process was killed prior to its calling
stopSelf
.
In the latter case, a call to
onStartCommand
will be made, passing in the initial Intent whose
processing did not properly complete.
Note that each mode requires you to explicitly stop your Service, through
stopService
or
stopSelf
flag
parameter can be used to discover how the Service was started. In particular you can use the
code snippet shown in Listing 9-2 to determine if either of the following cases is true:
➤
START_FLAG_REDELIVERY
Indicates that the Intent parameter is a redelivery caused by the
system run time’s having terminated the Service before it was explicitly stopped by a call to
stopSelf
.
➤
START_FLAG_RETRY
Indicates that the Service has been restarted after an abnormal termina-
tion. Passed in when the Service was previously set to
START_STICKY
.
LISTING 9-2: Determining the cause of a system start
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if ((flags & START_FLAG_RETRY)==0){
// TODO If it’s a restart, do something.
}
else {
// TODO Alternative background process.
}
return Service.START_STICKY;
}
Registering a Service in the Manifest
Once you’ve constructed a new Service, you have to register it in the application manifest.
Do this by including a
<service>
startService
; you can either use an action to implicitly start a Service with the
appropriate Intent Receiver registered, or you can explicitly specify the Service using its class. If the
Service requires permissions that your application does not have, the call to
startService
will throw a
SecurityException
.
In both cases you can pass values in to the Service’s
onStart
handler by adding extras to the Intent, as
shown in Listing 9-3, which demonstrates both techniques available for starting a Service.
LISTING 9-3: Starting a Service
// Implicitly start a Service
Intent myIntent = new Intent(MyService.ORDER_PIZZA);
myIntent.putExtra("TOPPING", "Margherita");
startService(myIntent);
// Explicitly start a Service
startService(new Intent(this, MyService.class));
To use this example you would need to include a
MY_ACTION
constant in the
MyService
class and use an Intent Filter to register the Service as a provider of
MY_ACTION
.
To stop a Service use
stopService
, passing an Intent that defines the Service to stop. Listing 9-4 first
starts and then stops a Service both explicitly and by using the component name returned from a call
❘
291
Later in this chapter you’ll build additional functionality within this Service,
starting by moving the network lookup and XML parsing to a background thread.
Later you’ll use Toasts and Notifications to alert users of new earthquakes.
1. Start by creating a new
EarthquakeService
that extends
Service
.
package com.paad.earthquake;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import java.util.Timer;
import java.util.TimerTask;
public class EarthquakeService extends Service {
@Override
public void onCreate() {
// TODO: Initialize variables, get references to GUI objects
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
2. Add this new Service to the manifest by adding a new
service
tag within the
application
ContentValues values = new ContentValues();
values.put(EarthquakeProvider.KEY_DATE,
_quake.getDate().getTime());
values.put(EarthquakeProvider.KEY_DETAILS, _quake.getDetails());