52862.fb2
A popular feature on current-era mobile devices is GPS capability, so the device can tell you where you are at any point in time. While the most popular use of GPS service is mapping and directions, there are other things you can do if you know your location. For example, you might set up a dynamic chat application where the people you can chat with are based on physical location, so you’re chatting with those you are nearest. Or you could automatically geotag posts to Twitter or similar services.
GPS is not the only way a mobile device can identify your location. Alternatives include the following:
• The European equivalent to GPS, called Galileo, which is still under development at the time of this writing
• Cell-tower triangulation, where your position is determined based on signal strength to nearby cell towers
• Proximity to public WiFi “hotspots” that have known geographic locations
Android devices may have one or more of these services available to them. You, as a developer, can ask the device for your location, plus details on what providers are available. There are even ways for you to simulate your location in the emulator, for use in testing your location-enabled applications.
Android devices can have access to several different means of determining your location. Some will have better accuracy than others. Some may be free, while others may have a cost associated with them. Some may be able to tell you more than just your current position, such as your elevation over sea level, or your current speed.
Android, therefore, has abstracted all this out into a set of LocationProvider
objects. Your Android environment will have zero or more LocationProvider
instances, one for each distinct locating service that is available on the device. Providers know not only your location, but their own characteristics, in terms of accuracy, cost, etc.
You, as a developer, will use a LocationManager
, which holds the LocationProvider
set, to figure out which LocationProvider
is right for your particular circumstance. You will also need the ACCESS_LOCATION
permission in your application, or the various location APIs will fail due to a security violation. Depending on which location providers you wish to use, you may need other permissions as well, such as ACCESS_COARSE_LOCATION
or ACCESS_FINE_LOCATION
.
The obvious thing to do with a location service is to figure out where you are right now.
To do that, you need to get a LocationManager
— call getSystemService(LOCATION_SERVICE)
from your activity or service and cast it to be a LocationManager
.
The next step to find out where you are is to get the name of the LocationProvider
you want to use. Here, you have two main options:
• Ask the user to pick a provider.
• Find the best-match provider based on a set of criteria.
If you want the user to pick a provider, calling getProviders()
on the LocationManager
will give you a List
of providers, which you can then present to the user for selection.
Or, you can create and populate a Criteria
object, stating the particulars of what you want out of a LocationProvider
, such as the following:
• setAltitudeRequired()
to indicate if you need the current altitude or not
• setAccuracy()
to set a minimum level of accuracy, in meters, for the position
• setCostAllowed()
to control if the provider must be free or not (i.e., if it can incur a cost on behalf of the device user)
Given a filled-in Criteria
object, call getBestProvider()
on your LocationManager
, and Android will sift through the criteria and give you the best answer. Note that not all of your criteria may be met — all but the monetary-cost criterion might be relaxed if nothing matches.
You are also welcome to hard-wire in a LocationProvider
name (e.g., gps
), perhaps just for testing purposes.
Once you know the name of the LocationProvider
, you can call getLastKnownPosition()
to find out where you were recently. Note, however, that “recently” might be fairly out-of-date (e.g., if the phone was turned off) or even null if there has been no location recorded for that provider yet. On the other hand, getLastKnownPosition()
incurs no monetary or power cost, since the provider does not need to be activated to get the value.
These methods return a Location
object, which can give you the latitude and longitude of the device in degrees as a Java double
. If the particular location provider offers other data, you can get at that as well:
• For altitude, hasAltitude()
will tell you if there is an altitude value, and getAltitude()
will return the altitude in meters.
• For bearing (i.e., compass-style direction), hasBearing()
will tell you if there is a bearing available, and getBearing()
will return it as degrees east of true north.
• For speed, hasSpeed()
will tell you if the speed is known, and getSpeed()
will return the speed in meters per second.
A more likely approach to getting the Location
from a LocationProvider
, though, is to register for updates, as described in the next section.
Not all location providers are necessarily immediately responsive. GPS, for example, requires activating a radio and getting a fix from the satellites before you get a location. That is why Android does not offer a getMeMyCurrentLocationNow()
method. Combine that with the fact that your users may well want their movements to be reflected in your application, and you are probably best off registering for location updates and using that as your means of getting the current location.
The Weather
and WeatherPlus
sample applications (available in the Source Code area at http://apress.com) show how to register for updates — call requestLocationUpdates()
on your LocationManager
instance. This takes four parameters:
1. The name of the location provider you wish to use
2. How long, in milliseconds, must have elapsed before we might get a location update
3. How far, in meters, the device must have moved before we might get a location update
4. A LocationListener
that will be notified of key location-related events, as shown in the following code:
LocationListener onLocationChange = new LocationListener() {
public void onLocationChanged(Location location) {
updateForecast(location);
}
public void onProviderDisabled(String provider) {
// required for interface, not used
}
public void onProviderEnabled(String provider) {
// required for interface, not used
}
public void onStatusChanged(String provider, int status,
Bundle extras) {
// required for interface, not used
}
};
Here, all we do is call updateForecast()
with the Location
supplied to the onLocationChanged()
callback method. The updateForecast()
implementation, as shown in Chapter 30, builds a Web page with the current forecast for the location and sends a broadcast so the activity knows an update is available.
When you no longer need the updates, call removeUpdates()
with the LocationListener
you registered.
Sometimes you want to know not where you are now, or even when you move, but when you get to where you’re going. This could be an end destination, or it could be getting to the next step on a set of directions so you can give the user the next turn.
To accomplish this, LocationManager
offers addProximityAlert()
. This registers a PendingIntent
, which will be fired off when the device gets within a certain distance of a certain location. The addProximityAlert()
method takes the following as parameters:
• The latitude and longitude of the position that you are interested in.
• A radius, specifying how close you should be to that position for the Intent to be raised.
• A duration for the registration, in milliseconds — after this period, the registration automatically lapses. A value of -1 means the registration lasts until you manually remove it via removeProximityAlert()
.
• The PendingIntent
to be raised when the device is within the “target zone” expressed by the position and radius.
Note that it is not guaranteed that you will actually receive an Intent if there is an interruption in location services or if the device is not in the target zone during the period of time the proximity alert is active. For example, if the position is off by a bit and the radius is a little too tight, the device might only skirt the edge of the target zone, or go by so quickly that the device’s location isn’t sampled while in the target zone.
It is up to you to arrange for an activity or intent receiver to respond to the Intent
you register with the proximity alert. What you then do when the Intent
arrives is up to you: set up a notification (e.g., vibrate the device), log the information to a content provider, post a message to a Web site, etc. Note that you will receive the Intent
whenever the position is sampled and you are within the target zone — not just upon entering the zone. Hence, you will get the Intent
several times, perhaps quite a few times depending on the size of the target zone and the speed of the device’s movement.
The Android emulator does not have the ability to get a fix from GPS, triangulate your position from cell towers, or identify your location by some nearby WiFi signal. So, if you want to simulate a moving device, you will need to have some means of providing mock location data to the emulator.
For whatever reason, this particular area has undergone significant changes as Android itself has evolved. It used to be that you could provide mock location data within your application, which was very handy for demonstration purposes. Alas, those options have all been removed as of Android 1.0.
One likely option for supplying mock location data is the Dalvik Debug Monitor Service (DDMS). This is an external program, separate from the emulator, which can feed the emulator single location points or full routes to traverse, in a few different formats. DDMS is described in greater detail in Chapter 37.
One of Google’s most popular services — after search, of course — is Google Maps, where you can find everything from the nearest pizza parlor to directions from New York City to San Francisco (only 2,905 miles!) to street views and satellite imagery.
Android, not surprisingly, integrates Google Maps. There is a mapping activity available to users straight off the main Android launcher. More relevant to you, as a developer, are MapView
and MapActivity
, which allow you to integrate maps into your own applications. Not only can you display maps, control the zoom level, and allow people to pan around, but you can tie in Android’s location-based services to show where the device is and where it is going.
Fortunately, integrating basic mapping features into your Android project is fairly easy. However, there is a fair bit of power available to you, if you want to get fancy.
Google Maps, particularly when integrated into third party applications, requires agreeing to a fairly lengthy set of legal terms. These terms include clauses that you may find unpalatable.
If you are considering Google Maps, please review these terms closely to determine if your intended use will not run afoul of any clauses. You are strongly recommended to seek professional legal counsel if there are any potential areas of conflict.
Also, keep your eyes peeled for other mapping options, based off of other sources of map data, such as OpenStreetMap.[32]
Far and away the simplest way to get a map into your application is to create your own subclass of MapActivity
. Like ListActivity
, which wraps up some of the smarts behind having an activity dominated by a ListView
, MapActivity
handles some of the nuances of setting up an activity dominated by a MapView
.
In your layout for the MapActivity
subclass, you need to add an element named, at the time of this writing, com.google.android.maps.MapView
. This is the “longhand” way to spell out the names of widget classes, by including the full package name along with the class name. This is necessary because MapView
is not in the com.google.android.widget
namespace. You can give the MapView
widget whatever android:id
attribute value you want, plus handle all the layout details to have it render properly alongside your other widgets.
However, you do need to have:
• android:apiKey
, which in production will need to be a Google Maps API key — more on this here
• android:clickable="true"
, if you want users to be able to click and pan through your map
For example, from the Maps/NooYawk
sample application, here is the main layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.google.android.maps.MapView android:id="@+id/map"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:apiKey="<YOUR_API_KEY>"
android:clickable="true" />
<LinearLayout android:id="@+id/zoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" />
</RelativeLayout>
We’ll cover that mysterious zoom LinearLayout
and the apiKey
in later sections of this chapter. In addition, you will need a couple of extra things in your AndroidManifest.xml
file:
• The INTERNET
and ACCESS_COARSE_LOCATION
permissions
• Inside your <application>
, a <uses-library>
element with android:name="com.google.android.maps"
, to indicate you are using one of the optional Android APIs
Here is the AndroidManifest.xml
file for NooYawk:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.commonsware.android.maps">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application android:label="@string/app_name">
<uses-library android:name="com.google.android.maps" />
<activity android:name=".NooYawk" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
That is pretty much all you need for starters, plus to subclass your activity from MapActivity
. If you were to do nothing else, and built that project and tossed it in the emulator, you’d get a nice map of the world. Note, however, that MapActivity
is abstract — you need to implement isRouteDisplayed()
to indicate if you are supplying some sort of driving directions or not.
In theory, the user could pan around the map using the directional pad. However, that’s not terribly useful when the user has the whole world in her hands.
Since a map of the world is not much good by itself, we need to add a few things…
You can find your MapView
widget by findViewById()
, no different than any other widget. The widget itself then offers a getMapController()
method. Between the MapView
and MapController
, you have a fair bit of capability to determine what the map shows and how it behaves. The following sections cover zoom and center, the features you will most likely want to use.
The map of the world you start with is rather broad. Usually, people looking at a map on a phone will be expecting something a bit narrower in scope, such as a few city blocks.
You can control the zoom level directly via the setZoom()
method on the MapController
. This takes an integer representing the level of zoom, where 1 is the world view and 21 is the tightest zoom you can get. Each level is a doubling of the effective resolution: 1 has the equator measuring 256 pixels wide, while 21 has the equator measuring 268,435,456 pixels wide. Since the phone’s display probably doesn’t have 268,435,456 pixels in either dimension, the user sees a small map focused on one tiny corner of the globe. A level of 16 will show you several city blocks in each dimension and is probably a reasonable starting point for you to experiment with.
If you wish to allow users to change the zoom level, you will need to do a few things:
• First, pick a spot on the screen where you want the zoom controls to appear. These are not huge, and they only appear when being used, so they can overlay the actual map itself if you choose. In the layout previously shown, for example, the zoom controls are placed over the map, in the lower-left corner of the screen. You should use a LinearLayout
or other simple container for the zoom controls’ position in your layout.
• In your activity’s onCreate()
method, get your zoom controls’ container via findViewById()
.
• Add the result o f map.getZoomControls()
to that container.
For example, here are the lines from the NooYawk
activity’s onCreate()
method that accomplish the latter points:
ViewGroup zoom = (ViewGroup)findViewById(R.id.zoom);
zoom.addView(map.getZoomControls());
Then, you can manually get the zoom controls to appear by calling displayZoomControls()
on your MapView
, or they will automatically appear when the user pans the map as seen in Figure 34-1.
Figure 34-1. Map with zoom indicator and compass rose
Typically, you will need to control what the map is showing, beyond the zoom level, such as the user’s current location, or a location saved with some data in your activity. To change the map’s position, call setCenter()
on the MapController
.
This takes a GeoPoint
as a parameter. A GeoPoint
represents a location, via latitude and longitude. The catch is that the GeoPoint
stores latitude and longitude as integers representing the actual latitude and longitude multiplied by 1E6
. This saves a bit of memory versus storing a float
or double
, and it probably speeds up some internal calculations Android needs to do to convert the GeoPoint
into a map position. However, it does mean you have to remember to multiply the “real world” latitude and longitude by 1E6
.
Just as the Google Maps you use on your full-size computer can display satellite imagery, so too can Android maps.
MapView
offers toggleSatellite()
, which, as the names suggest, toggles on and off the satellite perspective on the area being viewed. You can have the user trigger these via an options menu or, in the case of NooYawk
, via keypresses:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_S) {
map.setSatellite(!map.isSatellite());
return(true);
} else if (keyCode == KeyEvent.KEYCODE_Z) {
map.displayZoomControls(true);
return(true);
}
return(super.onKeyDown(keyCode, event));
}
If you have ever used the full-size edition of Google Maps, you are probably used to seeing things overlaid atop the map itself, such as “push-pins” indicating businesses near the location being searched. In map parlance — and, for that matter, in many serious graphic editors — the push-pins are on a separate layer than the map itself, and what you are seeing is the composition of the push-pin layer atop the map layer.
Android’s mapping allows you to create layers as well, so you can mark up the maps as you need to based on user input and your application’s purpose. For example, NooYawk
uses a layer to show where select buildings are located in the island of Manhattan.
Any overlay you want to add to your map needs to be implemented as a subclass of Overlay
. There is an ItemizedOverlay
subclass available if you are looking to add push-pins or the like; ItemizedOverlay
simplifies this process.
To attach an overlay class to your map, just call getOverlays()
on your MapView
and add()
your Overlay
instance to it:
marker.setBounds(0, 0, marker.getIntrinsicWidth(),
marker.getIntrinsicHeight());
map.getOverlays().add(new SitesOverlay(marker));
We will explain that marker in just a bit.
As the name suggests, ItemizedOverlay
allows you to supply a list of points of interest to be displayed on the map — specifically, instances of OverlayItem
. The overlay, then, handles much of the drawing logic for you. Here are the minimum steps to make this work:
• First, override ItemizedOverlayOverlayItem
as your own subclass (in this example, SitesOverlay
)
• In the constructor, build your roster of OverlayItem
instances, and call populate()
when they are ready for use by the overlay
• Implement size()
to return the number of items to be handled by the overlay
• Override createItem()
to return OverlayItem
instances given an index
• When you instantiate your ItemizedOverlay
subclass, provide it with a Drawable
that represents the default icon (e.g., push-pin) to display for each item
The marker from the NooYawk
constructor is the Drawable used for the last bullet — it shows a push-pin, as illustrated in Figure 34-1 earlier in this chapter.
You may also wish to override draw()
to do a better job of handling the shadow for your markers. While the map will handle casting a shadow for you, it appears you need to provide a bit of assistance for it to know where the “bottom” of your icon is, so it can draw the shadow appropriately.
For example, here is SitesOverlay
:
private class SitesOverlay extends ItemizedOverlay<OverlayItem> {
private List<OverlayItem> items = new ArrayList<OverlayItem>();
private Drawable marker = null;
public SitesOverlay(Drawable marker) {
super(marker);
this.marker = marker;
items.add(new OverlayItem(getPoint(40.748963847316034,
-73.96807193756104), "UN", "United Nations"));
items.add(new OverlayItem(getPoint(40.76866299974387,
-73.98268461227417), "Lincoln Center",
"Home of Jazz at Lincoln Center"));
items.add(new OverlayItem(getPoint(40.765136435316755,
-73.97989511489868), "Carnegie Hall",
"Where you go with practice, practice, practice"));
items.add(new OverlayItem(getPoint(40.70686417491799,
-74.01572942733765), "The Downtown Club",
"Original home of the Heisman Trophy"));
populate();
}
@Override
protected OverlayItem createItem(int i) {
return(items.get(i));
}
@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
boundCenterBottom(marker);
}
@Override
protected boolean onTap(int i) {
Toast.makeText(NooYawk.this,
items.get(i).getSnippet(), Toast.LENGTH_SHORT).show();
return(true);
}
@Override
public int size() {
return(items.size());
}
}
An Overlay
subclass can also implement onTap()
, to be notified when the user taps on the map, so the overlay can adjust what it draws. For example, in full-size Google Maps, clicking on a push-pin pops up a bubble with information about the business at that pin’s location. With onTap()
, you can do much the same in Android.
The onTap()
method for ItemizedOverlay
receives the index of the OverlayItem
that was clicked. It is up to you to do something worthwhile with this event.
In the case of SitesOverlay
, as previously shown, onTap()
looks like this:
@Override
protected boolean onTap(int i) {
Toast.makeText(NooYawk.this,
items.get(i).getSnippet(), Toast.LENGTH_SHORT).show();
return(true);
}
Here, we just toss up a short Toast
with the “snippet” from the OverlayItem
, returning true to indicate we handled the tap.
Android has a built-in overlay to handle two common scenarios:
• Showing where you are on the map, based on GPS or other location-providing logic
• Showing where you are pointed, based on the built-in compass sensor, where available
All you need to do is create a MyLocationOverlay
instance, add it to your MapView
’s list of overlays, and enable and disable the desired features at appropriate times.
The “at appropriate times” notion is for maximizing battery life. There is no sense in updating locations or directions when the activity is paused, so it is recommended that you enable these features in onResume()
and disable them in onPause()
.
For example, NooYawk
will display a compass rose using MyLocationOverlay
. To do this, we first need to create the overlay and add it to the list of overlays:
me = new MyLocationOverlay(this, map);
map.getOverlays().add(me);
Then, we enable and disable the compass rose as appropriate:
@Override
public void onResume() {
super.onResume();
me.enableCompass();
}
@Override
public void onPause() {
super.onPause();
me.disableCompass();
}
If you actually download the source code for the book, compile the NooYawk project, install it in your emulator, and run it, you will probably see a screen with a grid and a couple of pushpins, but no actual maps.
That’s because the API key in the source code is invalid for your development machine. Instead, you will need to generate your own API key(s) for use with your application.
Full instructions for generating API keys, for development and production use, can be found on the Android Web site.[33] In the interest of brevity, let’s focus on the narrow case of getting NooYawk running in your emulator. Doing this requires the following steps:
1. Visit the API key signup page and review the terms of service.
2. Re-read those terms of service and make really, really sure you want to agree to them.
3. Find the MD5 digest of the certificate used for signing your debug-mode applications (described in detail in the following).
4. On the API key signup page, paste in that MD5 signature and submit the form.
5. On the resulting page, copy the API key and paste it as the value of apiKey
in your MapView
-using layout.
The trickiest part is finding the MD5 signature of the certificate used for signing your debug-mode applications… and much of the complexity is merely in making sense of the concept.
All Android applications are signed using a digital signature generated from a certificate. You are automatically given a debug certificate when you set up the SDK, and there is a separate process for creating a self-signed certificate for use in your production applications. This signature process involves the use of the Java keytool
and jarsigner
utilities. For the purposes of getting your API key, you only need to worry about keytool
.
To get your MD5 digest of your debug certificate, if you are on OS X or Linux, use the following command:
keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass
android -keypass android
On other development platforms, you will need to replace the value of the -keystore
switch with the location for your platform and user account:
• Windows XP: C:\Documents and Settings\<user>\Local Settings\Application Data\Android\debug.keystore
• Windows Vista: C:\Users\<user>\AppData\Local\Android\debug.keystore
(where <user>
is your account name)
The second line of the output contains your MD5 digest, as a series of pairs of hex digits separated by colons.
Many, if not most, Android devices will be phones. As such, not only will users be expecting to place and receive calls using Android, but you will have the opportunity to help them place calls, if you wish.
Why might you want to?
• Maybe you are writing an Android interface to a sales management application (a la Salesforce.com
) and you want to offer users the ability to call prospects with a single button click, and without them having to keep those contacts both in your application and in the phone’s contacts application
• Maybe you are writing a social networking application, and the roster of phone numbers that you can access shifts constantly, so rather than try to “sync” the social network contacts with the phone’s contact database, you let people place calls directly from your application
• Maybe you are creating an alternative interface to the existing contacts system, perhaps for users with reduced motor control (e.g., the elderly), sporting big buttons and the like to make it easier for them to place calls
Whatever the reason, Android has the means to let you manipulate the phone just like any other piece of the Android system.
To get at much of the phone API, you use the TelephonyManager
. That class lets you do things like:
• Determine if the phone is in use via getCallState()
, with return values of CALL_STATE_IDLE
(phone not in use), CALL_STATE_RINGING
(call requested but still being connected), and CALL_STATE_OFFHOOK
(call in progress)
• Find out the SIM ID (IMSI) via getSubscriberId()
• Find out the phone type (e.g., GSM) via getPhoneType()
or find out the data connection type (e.g., GPRS, EDGE) via getNetworkType()
You can also initiate a call from your application, such as from a phone number you obtained through your own Web service. To do this, simply craft an ACTION_DIAL Intent
with a Uri
of the form tel:NNNNN
(where NNNNN
is the phone number to dial) and use that Intent
with startActivity()
. This will not actually dial the phone; rather, it activates the dialer activity, from which the user can then press a button to place the call.
For example, let’s look at the Phone/Dialer
sample application. Here’s the crude-but-effective layout:
<?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"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Number to dial:"
/>
<EditText android:id="@+id/number"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:cursorVisible="true"
android:editable="true"
android:singleLine="true"
/>
</LinearLayout>
<Button android:id="@+id/dial"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Dial It!"
/>
</LinearLayout>
We have a labeled field for typing in a phone number, plus a button for dialing said number.
The Java code simply launches the dialer using the phone number from the field:
package com.commonsware.android.dialer;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class DialerDemo extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
final EditText number = (EditText)findViewById(R.id.number);
Button dial = (Button)findViewById(R.id.dial);
dial.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
String toDial = "tel:" + number.getText().toString();
startActivity(new Intent(Intent.ACTION_DIAL,
Uri.parse(toDial)));
}
});
}
}
The activity’s own UI is not that impressive as shown in Figure 35-1.
Figure 35-1. The DialerDemo sample application, as initially launched
However, the dialer you get from clicking the dial button is better, showing you the number you are about to dial in Figure 35-2.
Figure 35-2. The Android Dialer activity, as launched from DialerDemo
One of the firms behind the Open Handset Alliance — Google — has a teeny-weeny Web search service, one you might have heard of in passing. Given that, it’s not surprising that Android has some built-in search capabilities. Specifically, Android has baked in the notion of searching not only on the device for data, but over the air to Internet sources of data. Your applications can participate in the search process by triggering searches or perhaps by allowing your application’s data to be searched.
Note that this is fairly new to the Android platform, and so some shifting in the APIs is likely.
There are two types of search in Android: local and global. Local search searches within the current application; global search searches the Web via Google’s search engine. You can initiate either type of search in a variety of ways, including the following:
• You can call onSearchRequested()
from a button or menu choice, which will initiate a local search (unless you override this method in your activity).
• You can directly call startSearch()
to initiate a local or global search, including optionally supplying a search string to use as a starting point.
• You can elect to have keyboard entry kick off a search via setDefaultKeyMode()
, for either local search (setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL)
) or global search (setDefaultKeyMode(DEFAULT_KEYS_SEARCH_GLOBAL)
).
In either case, the search appears as a set of UI components across the top of the screen, with your activity blurred underneath it (see Figures 36-1 and 36-2).
Figure 36-1. The Android local search pop-up
Figure 36-2. The Android global search pop-up, showing a drop-down with previous searches
Over the long haul, there will be two flavors of search available via the Android search system:
• Query-style search, where the user’s search string is passed to an activity that is responsible for conducting the search and displaying the results
• Filter-style search, where the user’s search string is passed to an activity on every key press, and the activity is responsible for updating a displayed list of matches
Since the latter approach is under heavy development right now by the Android team, let’s focus on the first one.
The first thing you’ll want to do if you want to support query-style search in your application is to create a search activity. While it might be possible to have a single activity be both opened from the launcher and opened from a search, that might prove somewhat confusing to users. Certainly, for the purposes of learning the techniques, having a separate activity is cleaner.
The search activity can have any look you want. In fact, other than watching for queries, a search activity looks, walks, and talks like any other activity in your system.
All the search activity needs to do differently is check the intents supplied to onCreate()
(via getIntent()
) and onNewIntent()
to see if one is a search, and, if so, to do the search and display the results.
For example, let’s look at the Search/Lorem
sample application (available in the Source Code section of http://apress.com). This starts off as a clone of the list-of-lorem-ipsum-words application that we first built back when showing off the ListView
container in Chapter 8, then with XML resources in Chapter 19. Now we update it to support searching the list of words for ones containing the search string.
The main activity and the search activity share a common layout: a ListView
plus a TextView
showing the selected entry:
<?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" >
<TextView
android:id="@+id/selection"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
/>
</LinearLayout>
In terms of Java code, most of the guts of the activities are poured into an abstract LoremBase
class:
abstract public class LoremBase extends ListActivity {
abstract ListAdapter makeMeAnAdapter(Intent intent);
private static final int LOCAL_SEARCH_ID = Menu.FIRST+1;
private static final int GLOBAL_SEARCH_ID = Menu.FIRST+2;
private static final int CLOSE_ID = Menu.FIRST+3;
TextView selection;
ArrayList<String> items = new ArrayList<String>();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
selection = (TextView)findViewById(R.id.selection);
try {
XmlPullParser xpp = getResources().getXml(R.xml.words);
while (xpp.getEventType()!=XmlPullParser.END_DOCUMENT) {
if (xpp.getEventType()==XmlPullParser.START_TAG) {
if (xpp.getName().equals("word")) {
items.add(xpp.getAttributeValue(0));
}
}
xpp.next();
}
} catch (Throwable t) {
Toast
.makeText(this, "Request failed: " + t.toString(), 4000).show();
}
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
onNewIntent(getIntent());
}
@Override
public void onNewIntent(Intent intent) {
ListAdapter adapter = makeMeAnAdapter(intent);
if (adapter==null) {
finish();
} else {
setListAdapter(adapter);
}
}
public void onListItemClick(ListView parent, View v, int position,
long id) {
selection.setText(items.get(position).toString());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, LOCAL_SEARCH_ID, Menu.NONE, "Local Search")
.setIcon(android.R.drawable.ic_search_category_default);
menu.add(Menu.NONE, GLOBAL_SEARCH_ID, Menu.NONE, "Global Search")
.setIcon(R.drawable.search).setAlphabeticShortcut(SearchManager.MENU_KEY);
menu.add(Menu.NONE, CLOSE_ID, Menu.NONE, "Close")
.setIcon(R.drawable.eject).setAlphabeticShortcut('c');
return(super.onCreateOptionsMenu(menu));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case LOCAL_SEARCH_ID:
onSearchRequested();
return(true);
case GLOBAL_SEARCH_ID:
startSearch(null, false, null, true);
return(true);
case CLOSE_ID:
finish();
return(true);
}
return(super.onOptionsItemSelected(item));
}
}
This activity takes care of everything related to showing a list of words, even loading the words out of the XML resource. What it does not do is come up with the ListAdapter
to put into the ListView
— that is delegated to the subclasses.
The main activity — LoremDemo
— just uses a ListAdapter
for the whole word list:
package com.commonsware.android.search;
import android.content.Intent;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
public class LoremDemo extends LoremBase {
@Override ListAdapter makeMeAnAdapter(Intent intent) {
return(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, items));
}
}
The search activity, though, does things a bit differently. First, it inspects the Intent
supplied to the abstract makeMeAnAdapter()
method. That Intent comes from either onCreate()
or onNewIntent()
. If the intent is an ACTION_SEARCH
, then we know this is a search. We can get the search query and, in the case of this silly demo, spin through the loaded list of words and find only those containing the search string. That list then gets wrapped in a ListAdapter
and returned for display:
package com.commonsware.android.search;
import android.app.SearchManager;
import android.content.Intent;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import java.util.ArrayList;
import java.util.List;
public class LoremSearch extends LoremBase {
@Override
ListAdapter makeMeAnAdapter(Intent intent) {
ListAdapter adapter = null;
if (intent.getAction().equals(Intent.ACTION_SEARCH)) {
String query = intent.getStringExtra(SearchManager.QUERY);
List<String> results = searchItems(query);
adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, results);
setTitle("LoremSearch for: " + query);
}
return(adapter);
}
private List<String> searchItems(String query) {
List<String> results = new ArrayList<String>();
for (String item : items) {
if (item.indexOf(query) - 1) {
results.add(item);
}
}
return(results);
}
}
While this implements search, it doesn’t tie it into the Android search system. That requires a few changes to the auto-generated AndroidManifest.xml
file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.commonsware.android.search">
<application>
<activity android:name=".LoremDemo" android:label="LoremDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.default_searchable"
android:value=".LoremSearch" />
</activity>
<activity
android:name=".LoremSearch"
android:label="LoremSearch"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
</application>
</manifest>
The changes needed are as follows:
1. The LoremDemo
main activity gets a meta-data
element, with an android:name
of android.app.default_searchable
and a android:value
of the search implementation class (.LoremSearch
).
2. The LoremSearch
activity gets an intent filter for android.intent.action.SEARCH
, so search intents will be picked up.
3. The LoremSearch
activity is set to have android:launchMode="singleTop"
, which means at most one instance of this activity will be open at any time so we don’t wind up with a whole bunch of little search activities cluttering up the activity stack.
4. The LoremSearch
activity gets a meta-data
element, with an android:name
of android.app.searchable
and a android:value
of an XML resource containing more information about the search facility offered by this activity (@xml/searchable
).
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/searchLabel"
android:hint="@string/searchHint" />
That XML resource provides two bits of information today:
• What name should appear in the search domain button to the right of the search field, identifying to the user where they are searching (android:label
)
• What hint text should appear in the search field, to give the user a clue as to what they should be typing in (android:hint
)
Given all that, search is now available — Android knows your application is searchable and what search domain to use when searching from the main activity, and the activity knows how to do the search.
The options menu for this application has both local and global search options. In the case of local search, we just call onSearchRequested()
; in the case of global search, we call startSearch()
with true in the last parameter, indicating the scope is global.
Typing in a letter or two then clicking Search, will bring up the search activity and the subset of words containing what you typed, with your search query in the activity title bar. You can get the same effect if you just start typing in the main activity, since it is set up for triggering a local search.
The Android SDK is more than a library of Java classes and API calls. It also includes a number of tools to assist in application development.
Much of the focus has been on the Eclipse plug-in, to integrate Android development with that IDE. Secondary emphasis has been placed on the plug-in’s equivalents for use in other IDEs or without an IDE, such as adb for communicating with a running emulator.
This chapter will cover other tools beyond those two groups.
Android comes with a Hierarchy Viewer tool, designed to help you visualize your layouts as they are seen in a running activity in a running emulator. For example, you can determine how much space a certain widget is taking up, or try to find where a widget is hiding that does not appear on the screen.
To use the Hierarchy Viewer, you first need to fire up your emulator, install your application, launch your activity, and navigate to the spot you wish to examine. As you can see from Figure 37-1, for illustration purposes, we’ll use the ReadWrite demo application we introduced back in Chapter 18.
Figure 37-1. ReadWrite demo application
You can launch the Hierarchy Viewer via the hierarchyviewer
program, found in the tools/
directory in your Android SDK installation. This brings up the main Hierarchy Viewer window shown in Figure 37-2.
Figure 37-2. Hierarchy Viewer main window
The list on the left shows the various emulators you have opened. The number after the hyphen should line up with the number in parentheses in your emulator’s title bar.
Clicking on an emulator shows, on the right, the list of windows available for examination as you can see in Figure 37-3.
Figure 37-3. Hierarchy Viewer list of available windows
Note how there are many other windows besides our open activity, including the Launcher (i.e., the home screen), the Keyguard (i.e., the “Press Menu to Unlock” black screen you get when first opening the emulator), and so on. Your activity will be identified by application package and class (e.g., com.commonsware.android.files/...
).
Where things get interesting, though, is when you choose a window and click Load View Hierarchy. After a few seconds, the details spring into view, in a perspective called the Layout View (see Figure 37-4).
Figure 37-4. Hierarchy Viewer Layout View
The main area of the Layout View shows a tree of the various Views that make up your activity, starting from the overall system window and driving down into the individual UI widgets that users are supposed to interact with. You will see, on the lower-right branch of the tree, the LinearLayout
, Button
, and EditText
shown in the previous code listing. The remaining Views
are all supplied by the system, including the title bar.
Clicking on one of the views adds more information to this perspective and can be seen in Figure 37-5.
Figure 37-5. Hierarchy Viewer View properties
Now, in the upper-right region of the viewer, we see properties of the selected widget — in this case, the
Button. Alas, these properties do not appear to be editable.
Also, the widget is highlighted in red in the wireframe of the activity, shown beneath the properties (by default, views are shown as white outlines on a black background). This can help you ensure you have selected the right widget, if, say, you have several buttons and cannot readily tell from the tree what is what.
If you double-click on a View
in the tree, you are given a pop-up pane showing just that View
(and its children), isolated from the rest of your activity.
Down in the lower-left corner, you will see two toggle buttons, with the tree button initially selected. Clicking on the grid button puts the viewer in a whole new perspective, called the Pixel Perfect View (see Figure 37-6).
Figure 37-6. Hierarchy Viewer Pixel Perfect View
On the left, you see a tree representing the widgets and other Views
in your activity. In the middle, you see your activity (the Normal View), and on the right, you see a zoomed edition of your activity (the Loupe View).
What may not be initially obvious is that this imagery is live. Your activity is polled every so often, controlled by the Refresh Rate slider. Anything you do in the activity will then be reflected in the Pixel Perfect View’s Normal and Loupe Views.
The hairlines (cyan) overlaying the activity show the position being zoomed upon — just click on a new area to change where the Loupe View is inspecting. Of course, there is another slider to adjust how much the Loupe View is zoomed.
Another tool in the Android developer’s arsenal is the Dalvik Debug Monitor Service (DDMS). This is a “Swiss Army knife,” allowing you to do everything from browse log files, update the GPS location provided by emulator, simulate incoming calls and messages, and browse the onemulator storage to push and pull files.
DDMS has a wide range of uses, so this section will not try to cover them all, rather it will cover the most useful at the time of writing.
To launch DDMS, run the ddms
program inside the tools/
directory in your Android SDK distribution. It will initially display just a tree of emulators and running programs on the left (see Figure 37-7).
Figure 37-7. DDMS initial view
Clicking on an emulator allows you to browse the event log on the bottom and manipulate the emulator via the tabs on the right as shown in Figure 37-8.
Figure 37-8. DDMS, with emulator selected
Rather than use adb logcat
, DDMS lets you view your logging information in a scrollable table. Just highlight the emulator or device you want to monitor, and the bottom half of the screen shows the logs (see Figure 37-9).
Figure 37-9. DDMS logging filter
In addition, you can:
• Filter the Log tab by any of the five logging levels, shown as the V through E toolbar buttons.
• Create a custom filter, so you can view only those tagged with your application’s tag, by pressing the + toolbar button and completing the form. The name you enter in the form will be used as the name of another logging output tab in the bottom portion of the DDMS main window.
• Save the log information to a text file for later perusal, or for searching.
While you can use adb pull
and adb push
to get files to and from an emulator or device, DDMS lets you do that visually. Just highlight the emulator or device you wish to work with, then choose Device→File Explorer… from the main menu. That will bring up the typical directory browser seen in Figure 37-10.
Figure 37-10. DDMS File Explorer
Just browse to the file you want and click either the pull (left-most) or push (middle) toolbar button to transfer the file to/from your development machine. Or, click the delete (right-most) toolbar button to delete the file.
There are a few caveats to this:
• You cannot create directories through this tool. You will either need to use adb shell
or create them from within your application.
• While you can putter through most of the files on an emulator, you can access very little outside of /sdcard
on an actual device, due to Android security restrictions.
To take a screenshot of the Android emulator or device, simply press <Ctrl>-<S>
or choose Device→Screen capture… from the main menu. This will bring up a dialog box containing an image of the current screen shown in Figure 37-11.
Figure 37-11. DDMS screen capture
From here, you can click Save to save the image as a PNG file somewhere on your development machine, Refresh to update the image based on the current state of the emulator or device, or Done to close the dialog.
To use DDMS to supply location updates to your application, the first thing you must do is have your application use the gps LocationProvider
, as that is the one that DDMS is set to update.
Then, click on the Emulator Control tab and scroll down to the Location Controls section. Here, you will find a smaller tabbed pane with three options for specifying locations: Manual, GPX, and KML (see Figure 37-12).
Figure 37-12. DDMS location controls
The Manual tab is fairly self-explanatory: provide a latitude and longitude and click the Send button to submit that location to the emulator. The emulator, in turn will notify any location listeners of the new position.
Discussion of the GPX and KML options is beyond the scope of this book.
If you want to simulate incoming calls or SMS messages to the Android emulator, DDMS can handle that as well.
On the Emulator Control tab, above the Location Controls group, is the Telephony Actions group (see Figure 37-13).
Figure 37-13. DDMS telephony controls
To simulate an incoming call, fill in a phone number, choose the Voice radio button, and click Call. At that point, the emulator will show the incoming call, allowing you to accept it (via the green phone button) or reject it (via the red phone button) seen in Figure 37-14.
Figure 37-14. Simulated incoming call
To simulate an incoming text message, fill in a phone number, choose the SMS radio button, enter a message in the provided text area, and click Send. The text message will then appear as a notification as shown in Figure 37-15.
Figure 37-15. Simulated text message
Of course, you can click on the notification to view the message in the full-fledged Messaging application as you can see in Figure 37-16.
Figure 37-16. Simulated text message, in Messaging application
The T-Mobile G1 has a microSD card slot. Many other Android devices are likely to have similar forms of removable storage, which the Android platform refers to generically as an “SD card”.
SD cards are strongly recommended to be used by developers as the holding pen for large data sets: images, movie clips, audio files, etc. The T-Mobile G1, in particular, has a relatively paltry amount of on-board flash memory, so the more you can store on an SD card, the better.
Of course, the challenge is that, while the G1 has an SD card by default, the emulator does not. To make the emulator work like the G1, you need to create and “insert” an SD card into the emulator.
Rather than require emulators to somehow have access to an actual SD card reader and use actual SD cards, Android is set up to use card images. An image is simply a file that the emulator will treat as if it were an SD card volume. If you are used to disk images used with virtualization tools (e.g., VirtualBox), the concept is the same: Android uses a disk image representing the SD card contents.
To create such an image, use the mksdcard
utility, provided in the tools/
directory of your SDK installation. This takes two main parameters:
1. The size of the image, and hence the size of the resulting “card.” If you just supply a number, it is interpreted as a size in bytes. Alternatively, you can append K
or M
to the number to indicate a size in kilobytes or megabytes, respectively.
2. The filename under which to store the image.
For example, to create a 1GB SD card image, to simulate the G1’s SD card in the emulator, you could run:
mksdcard 1024M sdcard.img
To have your emulator use this SD card image, start the emulator with the -sdcard
switch, containing a fully-qualified path to the image file you created using mksdcard
. While there will be no visible impact — there is no icon or anything in Android showing that you have a card mounted — the /sdcard
path will now be available for reading and writing.
To put files on the /sdcard
, either use the File Explorer in DDMS or adb push
and adb pull
from the console.
Obviously, this book does not cover everything. And while your number-one resource (besides the book) is going to be the Android SDK documentation, you are likely to need information beyond what’s covered in either of those places.
Searching online for “android” and a class name is a good way to turn up tutorials that reference a given Android class. However, bear in mind that tutorials written before late August 2008 are probably written for the M5 SDK and, therefore, will require considerable adjustment to work properly in current SDKs.
Beyond randomly hunting around for tutorials, though, this chapter outlines some resources to keep in mind.
The official places to get assistance with Android are the Android Google Groups. With respect to the SDK, there are three to consider following:
• Android Beginners,[34] a great place to ask entry-level questions
• Android Developers,[35] best suited for more-complicated questions or ones that delve into less-used portions of the SDK
• Android Discuss,[36] designed for free-form discussion of anything Android-related, not necessarily for programming questions and answers
You might also consider these:
• The Android tutorials and programming forums at anddev.org[37]
• The #android
IRC channel on freenode
The source code to Android is now available. Mostly this is for people looking to enhance, improve, or otherwise fuss with the insides of the Android operating system. But, it is possible that you will find the answers you seek in that code, particularly if you want to see how some built-in Android component does its thing.
The source code and related resources can be found at the Android Open Source Project Web site.[38] Here, you can
• Download[39] or browse[40] the source code
• File bug reports[41] against the operating system itself
• Submit patches[42] and learn about the process for how such patches get evaluated and approved
• Join a separate set of Google Groups[43] for Android platform development
Ed Burnette, a nice guy who happened to write his own Android book, is also the manager of Planet Android,[44] a feed aggregator for a number of Android-related blogs. Subscribing to the planet’s feed will let you monitor quite a few Android-related blog posts, though not exclusively related to programming.
Now to focus more on programming-related Android-referencing blog posts; you can search DZone for “android” and subscribe to a feed[45] based on that search.
You might also consider keeping tabs on those mentioning Android in Twitter messages, such as by using a Summize feed.[46]
http://www.openstreetmap.org/
http://code.google.com/android/toolbox/apis/mapkey.html
http://groups.google.com/group/android-beginners
http://groups.google.com/group/android-developers
http://groups.google.com/group/android-discuss
http://anddev.org/
http://source.android.com
http://source.android.com/download
http://git.source.android.com/
http://source.android.com/report-bugs
http://source.android.com/submit-patches
http://source.android.com/discuss
http://www.planetandroid.com/
http://www.dzone.com/links/feed/search/android/rss.xml
http://summize.com/search.atom?lang=enq=android