Chapter     5

Interacting with Device Hardware and Media

Integrating application software with device hardware presents opportunities to create unique user experiences that only the mobile platform can provide. Capturing media by using the microphone and camera allows applications to incorporate a personal touch through a photo or recorded greeting. Integration of sensor and location data can help you develop applications to answer relevant questions such as “Where am I?” and “What am I looking at?”

In this chapter, we are going to investigate how to leverage the location, media, and sensor APIs provided by Android to add that unique value the mobile device brings into your applications.

5-1. Integrating Device Location

Problem

You want to leverage the device’s ability to report its current physical position in an application.

Solution

(API Level 1)

Utilize the background services provided by the Android LocationManager. One of the most powerful benefits that a mobile application can often provide to the user is the ability to add context by including information based on where that user is currently located. Applications may ask the LocationManager to provide updates of a device’s location either regularly or just when it is detected that the device has moved a significant distance.

When working with the Android location services, some care should be taken to respect both the device battery and the user’s wishes. Obtaining a fine-grained location fix by using a device’s GPS is a power-intensive process, and this can quickly drain the battery in the user’s device if left on continuously. For this reason, among others, Android allows the user to disable certain sources of location data, such as the device’s GPS. These settings must be observed when your application decides how it will obtain location.

Each location source also comes with a trade-off degree of accuracy. The GPS will return a more exact location (within a few meters) but will take longer to fix and use more power, whereas the network location will usually be accurate to a few kilometers but is returned much faster and uses less power. Consider the requirements of the application when deciding which sources to access; if your application wishes to display information about only the local city, perhaps GPS fixes are not necessary.

Important  When using location services in an application, keep in mind that android.permission.ACCESS_COARSE_LOCATION or android.permission.ACCESS_FINE_LOCATION must be declared in the application manifest. If you declare android.permission.ACCESS_FINE_LOCATION, you do not need both because it includes coarse permissions as well.

How It Works

When creating a simple monitor for user location in an activity or service, there are a few actions that we need to consider:

  1. Determine whether the source we want to use is enabled. If it’s not, decide whether to ask the user to enable it or to try another source.
  2. Register for updates using reasonable values for a minimum distance and update interval.
  3. Unregister for updates when they are no longer needed to conserve device power.

In Listing 5-1, we register an activity to listen for location updates while it is visible to the user and to display that location onscreen.

Listing 5-1. Activity Monitoring Location Updates

public class MainActivity extends Activity {
 
    private LocationManager mManager;
    private Location mCurrentLocation;
    
    private TextView mLocationView;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLocationView = new TextView(this);
        setContentView(mLocationView);
        
        mManager = (LocationManager)
                getSystemService(Context.LOCATION_SERVICE);
    }
    
    @Override
    public void onResume() {
        super.onResume();
        if(!mManager
               .isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            //Ask the user to enable GPS
            AlertDialog.Builder builder =
                    new AlertDialog.Builder(this);
            builder.setTitle("Location Manager");
            builder.setMessage(
                    "We would like to use your location, "
                    + "but GPS is currently disabled. "
                    + "Would you like to change these settings "
                    + "now?");
            builder.setPositiveButton("Yes",
                    new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog,
                        int which) {
                    //Launch settings, allowing user to change
                    Intent i = new Intent(Settings
                            .ACTION_LOCATION_SOURCE_SETTINGS);
                    startActivity(i);
                }
            });
            builder.setNegativeButton("No",
                    new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog,
                        int which) {
                    //No location service, no Activity
                    finish();
                }
            });
            builder.create().show();
        }
        //Get a cached location, if it exists
        mCurrentLocation = mManager.getLastKnownLocation(
                LocationManager.GPS_PROVIDER);
        updateDisplay();
        //Register for updates
        int minTime = 5000;
        float minDistance = 0;
        mManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                minTime,
                minDistance,
                mListener);
    }
    
    @Override
    public void onPause() {
        super.onPause();
        //Disable updates when we are not in the foreground
        mManager.removeUpdates(mListener);
    }
 
    private void updateDisplay() {
        if(mCurrentLocation == null) {
            mLocationView.setText("Determining Your Location...");
        } else {
            mLocationView.setText(
                    String.format("Your Location: %.2f, %.2f",
                            mCurrentLocation.getLatitude(),
                            mCurrentLocation.getLongitude()));
        }
    }
    
    private LocationListener mListener = new LocationListener() {
        //New location event
        @Override
        public void onLocationChanged(Location location) {
            mCurrentLocation = location;
            updateDisplay();
        }
        
        //The requested provider was disabled in settings
        @Override
        public void onProviderDisabled(String provider) { }
        
        //The requested provider was enabled in settings
        @Override
        public void onProviderEnabled(String provider) { }
 
        //Availability changes in the requested provider
        @Override
        public void onStatusChanged(String provider,
                int status, Bundle extras) { }
        
    };
}

This example chooses to work strictly with the device’s GPS to get location updates. Because it is a key element to the functionality of this activity, the first major task undertaken after each resume is to check whether the LocationManager.GPS_PROVIDER is still enabled. If, for any reason, the user has disabled this feature, we provide the opportunity to rectify this by asking whether the user would like to enable GPS. An application does not have the ability to do this for the user, so if the user agrees, we launch an activity by using the Intent action Settings.ACTION_LOCATION_SOURCE_SETTINGS, which brings up the device settings for enabling GPS.

EMULATING LOCATION CHANGES

If you are testing your application inside of the Android emulator, your application will not be able to receive real location data from any of the system providers. Using the DDMS tool in the SDK, however, you are able to inject location change events for the GPS_PROVIDER manually.

With DDMS active, select the Emulator Control tab and find the Location Controls section. A tabbed interface allows you to enter a latitude/longitude pair directly, or have series of them read from common file formats.

When entering a single value manually, a valid latitude and longitude must be entered in the text boxes. You may then click the Send button to inject that location as an event inside the selected emulator. Any applications registered to listen to location changes will also receive an update with this location value.

Once GPS is active and available, the activity registers a LocationListener to be notified of location updates. The LocationManager.requestLocationUpdates() method takes two major parameters of interest in addition to the provider type and destination listener:

  • minTime: The minimum time interval between updates, in milliseconds
    • Setting this to nonzero allows the location provider to rest for approximately the specified period before updating again.
    • This is a parameter to conserve power, and it should not be set to a value any lower than the minimum acceptable update rate.
  • minDistance: The distance the device must move before another update will be sent, in meters
    • Setting this to nonzero will block updates until it is determined that the device has moved at least this much.

In the example, we request that updates be sent no more often than every 5 seconds, with no regard for whether the location has changed significantly. When these updates arrive, the onLocationChanged() method of the registered listener is called. Notice that a LocationListener will also be notified when the status of different providers changes, although we are not utilizing those callbacks here.

Note  If you are receiving updates in a service or other background operation, Google recommends that the minimum time interval should be no less than 60,000 (60 seconds).

The example keeps a running reference to the latest location it received. Initially, this value is set to the last known location that the provider has cached by calling getLastKnownLocation(), which may return null if the provider does not have a cached location value. With each incoming update, the location value is reset and the user interface display is updated to reflect the new change.

5-2. Mapping Locations

Problem

You would like to display one or more locations on a map for the user. Additionally, you would like to display the user’s own location on that same map.

Solution

(API Level 8)

The simplest way to show the user a map is to create an Intent with the location data and pass it to the Android system to launch in a mapping application. We’ll look more in depth at this method for doing various tasks in Chapter 7. You can embed maps within your application by using MapView and MapFragment, provided by the Google Maps v2 library component of the Google Play Services library.

Important  Google Maps v2 is distributed as part of the Google Play Services library; it is not part of the native SDK at any platform level. However, any application targeting API Level 8 or later and devices inside the Google Play ecosystem can use the mapping library. For more information on including Google Play Services in your project, reference our guide in Chapter 1.

Obtaining an API Key

To get started with Maps v2, you will need to create an API project, enable the Maps v2 service inside of that project, and generate an API key to include in your application code. Without an API key, the mapping classes may be utilized, but no map tiles will be returned to the application. Follow these steps:

  1. Visit https://code.google.com/apis/console/ and log in with your Google account to access the Google API console,
  2. Select Create Project to make a new project for your maps. If you already have an existing project, you can add the Maps v2 service and keys to that if you prefer. In that case, select the project where you would like to add Maps v2.
  3. In the navigation panel, select Services , scroll down to Google Maps Android API v2, and enable the service.
  4. Select API Access in the navigation panel, and select Create new Android Key.
  5. Follow the onscreen instructions to add keystore signature/application package pairs to your key for the apps you want to use. In our case, the package name for the sample application is com.androidrecipes.mapper, and the signature comes from the debug key on your development machine, usually located at <USERHOME>/.android/debug.keystore.

Note  For more information on the SDK, and the most up-to-date instructions on getting an API key, visit https://developers.google.com/maps/documentation/android/start.

If you are running code in an emulator to test, that emulator must be built using an SDK target of Android 4.3 or later that includes the Google APIs for mapping to operate properly. Previous versions of the SDK bundled in the Maps v1 library rather than Google Play Services, so they will not work for testing.

If you create emulators from the command line, these targets are named Google Inc.:Google APIs:X, where X is the API version indicator. If you create emulators from inside an IDE (such as Eclipse), the target has a similar naming convention of Google APIs (Google Inc.) – X, where X is the API version indicator.

Meeting Manifest Requirements

Once you have obtained a valid API key, we need to include it in our AndroidManifest.xml file. The following code block must be inside the <application> element:

<meta-data
    android:name="com.google.android.maps.v2.API_KEY"
    android:value="YOUR_KEY_HERE" />

Additionally, Maps v2 has a device requirement of at least OpenGL ES 2.0. We can require this as a device feature by adding the following block inside your <manifest> element, typically placed just above the <application> element:

<!-- Maps v2 requires OpenGL ES 2.0 -->
<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

Finally, Maps v2 requires a set of permissions to talk to Google Play Services and render the map tiles. So we must add one more block inside the <manifest> element, typically placed just above the <application> element:

<!-- Permissions Required to Display a Map -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
    android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>
<uses-permission android:name=
    "com.google.android.providers.gsf.permission.READ_GSERVICES"
/>

Altogether, your manifest should look something like Listing 5-2.

Listing 5-2. Partial AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.androidrecipes.mapper"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="8"
        android:targetSdkVersion="16" />
 
    <!-- Permissions Required to Display a Map -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission
      android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission
      android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    />
    <uses-permission android:name=
      "com.google.android.providers.gsf.permission.READ_GSERVICES"
    />
 
    <!-- Maps v2 requires OpenGL ES 2.0 -->
    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />
 
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
 
        <!-- Activities, Services, Providers, and such -->
 
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="YOUR_KEY_HERE" />
    </application>
 
</manifest>

With the API key in hand and a suitable test platform in place, you are ready to begin.

How It Works

To display a map, simply create an instance of MapView or MapFragment. The API key is global to your application, so any instance of these elements will use this value. You do not need to add the key to each instance, as was the case with Maps v1.

Note  In addition to the permissions described previously, we must also add android.permission.ACCESS_FINE_LOCATION for this example. This is required only because this example is hooking back up to the LocationManager to get the cached location value.

Now, let’s look at an example that puts the last-known user location on a map and displays it. See Listing 5-3.

Listing 5-3. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="Map Of Your Location" />
    <RadioGroup
        android:id="@+id/group_maptype"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >
        <RadioButton
            android:id="@+id/type_normal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Normal Map" />
        <RadioButton
            android:id="@+id/type_satellite"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Satellite Map" />
    </RadioGroup>
    
    <fragment
        class="com.google.android.gms.maps.SupportMapFragment"
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

Note  When adding MapView or MapFragment to an XML layout, the fully qualified package name must be included, because the class does not exist in android.view or android.widget.

Here we have created a simple layout that includes a selector to toggle the map type displayed alongside a MapFragment instance. Listing 5-4 reveals the activity code to control the map.

Listing 5-4. Activity Displaying Cached Location

public class BasicMapActivity extends FragmentActivity implements
        RadioGroup.OnCheckedChangeListener {
 
    private SupportMapFragment mMapFragment;
    private GoogleMap mMap;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //Check if Google Play Services is up-to-date.
        switch (GooglePlayServicesUtil
                .isGooglePlayServicesAvailable(this)) {
            case ConnectionResult.SUCCESS:
                //Do nothing, move on
                break;
            case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
                Toast.makeText(this,
                        "Maps service requires an update, "
                        + "please open Google Play.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
            default:
                Toast.makeText(this,
                        "Maps are not available on this device.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
        }
        
        mMapFragment =
               (SupportMapFragment) getSupportFragmentManager()
                       .findFragmentById(R.id.map);
        mMap = mMapFragment.getMap();
        
        //See if our last known user location is valid, and center
        // the map around that point.  If not, use a default.
        LocationManager manager = (LocationManager)
                getSystemService(Context.LOCATION_SERVICE);
        Location location = manager.getLastKnownLocation(
                LocationManager.GPS_PROVIDER);
        
        LatLng mapCenter;
        if(location != null) {
            mapCenter = new LatLng(location.getLatitude(),
                    location.getLongitude());
        } else {
            //Use a default location
            mapCenter = new LatLng(37.4218,  -122.0840);
        }
        
        //Center and zoom the map simultaneously
        CameraUpdate newCamera =
                CameraUpdateFactory.newLatLngZoom(mapCenter, 13);
        mMap.moveCamera(newCamera);
        
        // Wire up the map type selector UI
        RadioGroup typeSelect =
                (RadioGroup) findViewById(R.id.group_maptype);
        typeSelect.setOnCheckedChangeListener(this);
        typeSelect.check(R.id.type_normal);
    }
 
    @Override
    public void onResume() {
        super.onResume();
        //Enable user location display on the map
        mMap.setMyLocationEnabled(true);
    }
    
    @Override
    public void onPause() {
        super.onResume();
        //Disable user location when not visible
        mMap.setMyLocationEnabled(false);
    }
    
    /** OnCheckedChangeListener Methods */
    
    @Override
    public void onCheckedChanged(RadioGroup group,
            int checkedId) {
        switch (checkedId) {
            case R.id.type_satellite:
                //Show the satellite map view
                mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
                break;
            case R.id.type_normal:
            default:
                //Show the normal map view
                mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
                break;
        }
    }
}

Our first order of business is to verify that the correct version of Google Play Services is installed on this device. Google manages the Google Play Services library automatically as the user of the device interacts with Google applications such as Google Play. Play Services is automatically updated in the background, so we need to verify at runtime that the user has what we need by using methods from GooglePlayServicesUtil. The result we receive from isGooglePlayServicesAvailable() will tell us whether the services are the correct version, need an update, or are even installed at all.

This activity takes the latest user location and centers the map on that point. All control of the map is done through a GoogleMap instance, which we obtain by callingMapFragment.getMap(). In this example, we use the map’s moveCamera()method to adjust the map display with a CameraUpdate object.

A CameraUpdate allows you to make adjustments to one or more components of the map display at once, such as modifying the zoom as well as the center point. The map’s zoom level is a discrete value between 2.0 and 21.0, with the lowest value making the entire world approximately 1,024dp wide, and each increasing level doubling the width of the world on the display.

When the user selects a different radio button, the map type is toggled between satellite view and the traditional map view. In addition to the values used in the example, other allowable map types are as follows:

  • MAP_TYPE_HYBRID: Displays map data (for example, streets and points of interest) over the top of the satellite view
  • MAP_TYPE_TERRAIN: Displays a map with terrain elevation contour lines

Finally, to enable the user location display and controls, we simply need to call setMyLocationEnabled() on the map. Because this method will enable location tracking and likely turn on elements such as the GPS, it should also be disabled when no longer needed (when the view is not visible). Figure 5-1 shows our basic map with the user location visible.

9781430263227_Fig05-01.jpg

Figure 5-1. Map of user location

This is a great start, but perhaps a little boring. To bring in some more interactivity, Recipe 5-3 will create markers and other annotations to the map, and show you how to customize them.

5-3. Annotating Maps

Problem

In addition to displaying a map centered on a specific location, your application needs to put an annotation down to mark a location more explicitly.

Solution

(API Level 8)

Add Marker objects and shape elements such as Circle and Polygon to the map. Marker objects are interactive objects defined by an icon that displays over a given location. That location can be fixed, or you can set the Marker to be dragged by the user to any point they wish. Each Marker can also respond to touch events such as taps and long-presses. Additionally, a Marker can be given metadata including a title and text snippet that should be displayed in a pop-up info window when the marker is tapped. These windows themselves are also customizable in their display.

Maps v2 also supports drawing discrete shape elements. These elements are not inherently interactive, though we will see it is not difficult to add the capability to interact with a shape. This feature can also be used to draw routes onto a map by using the Polyline shape, which does not attempt to draw as a closed, filled shape like the other options.

Important  Google Maps v2 is distributed as part of the Google Play Services library; it is not part of the native SDK at any platform level. However, any application targeting API Level 8 or later and devices inside the Google Play ecosystem can use the mapping library. For more information on including Google Play Services in your project, reference our guide in Chapter 1.

How It Works

Listings 5-5 and 5-6 show a new activity example with some markers added to the map. The XML layout is the same as we used in the previous recipe, so we won’t spend time dissecting its components again, but it is added here for completeness.

Listing 5-5. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="Map Of Your Location" />
    <RadioGroup
        android:id="@+id/group_maptype"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >
        <RadioButton
            android:id="@+id/type_normal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Normal Map" />
        <RadioButton
            android:id="@+id/type_satellite"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Satellite Map" />
    </RadioGroup>
    
    <fragment
        class="com.google.android.gms.maps.SupportMapFragment"
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

Listing 5-6. Activity Showing Map with Markers

public class MarkerMapActivity extends FragmentActivity implements
        RadioGroup.OnCheckedChangeListener,
        GoogleMap.OnMarkerClickListener,
        GoogleMap.OnMarkerDragListener,
        GoogleMap.OnInfoWindowClickListener,
        GoogleMap.InfoWindowAdapter {
 
    private SupportMapFragment mMapFragment;
    private GoogleMap mMap;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        // Check if Google Play Services is up-to-date.
        switch (GooglePlayServicesUtil
                .isGooglePlayServicesAvailable(this)) {
            case ConnectionResult.SUCCESS:
                // Do nothing, move on
                break;
            case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
                Toast.makeText(this,
                        "Maps service requires an update, "
                        + "please open Google Play.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
            default:
                Toast.makeText(this,
                        "Maps are not available on this device.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
        }
 
        mMapFragment =
                (SupportMapFragment) getSupportFragmentManager()
                        .findFragmentById(R.id.map);
        mMap = mMapFragment.getMap();
 
        // Monitor interaction with marker elements
        mMap.setOnMarkerClickListener(this);
        mMap.setOnMarkerDragListener(this);
        // Set our application to serve views for the info windows
        mMap.setInfoWindowAdapter(this);
        // Monitor click events on info windows
        mMap.setOnInfoWindowClickListener(this);
 
        // Google HQ 37.427,-122.099
        Marker marker = mMap.addMarker(new MarkerOptions()
                .position(new LatLng(37.4218, -122.0840))
                .title("Google HQ")
                // Show an image resource from our app as the marker
                .icon(BitmapDescriptorFactory
                        .fromResource(R.drawable.logo))
                //Reduce the opacity
                .alpha(0.6f));
        //Make this marker draggable on the map
        marker.setDraggable(true);
        
        // Subtract 0.01 degrees
        mMap.addMarker(new MarkerOptions()
                .position(new LatLng(37.4118, -122.0740))
                .title("Neighbor #1")
                .snippet("Best Restaurant in Town")
                // Show a default marker, in the default color
                .icon(BitmapDescriptorFactory.defaultMarker()));
 
        // Add 0.01 degrees
        mMap.addMarker(new MarkerOptions()
                .position(new LatLng(37.4318, -122.0940))
                .title("Neighbor #2")
                .snippet("Worst Restaurant in Town")
                // Show a default marker, with a blue tint
                .icon(BitmapDescriptorFactory
                        .defaultMarker(
                            BitmapDescriptorFactory.HUE_AZURE)));
 
        // Center and zoom the map simultaneously
        LatLng mapCenter = new LatLng(37.4218, -122.0840);
        CameraUpdate newCamera = CameraUpdateFactory
                .newLatLngZoom(mapCenter, 13);
        mMap.moveCamera(newCamera);
 
        // Wire up the map type selector UI
        RadioGroup typeSelect =
                (RadioGroup) findViewById(R.id.group_maptype);
        typeSelect.setOnCheckedChangeListener(this);
        typeSelect.check(R.id.type_normal);
    }
 
    /** OnCheckedChangeListener Methods */
 
    @Override
    public void onCheckedChanged(RadioGroup group,
            int checkedId) {
        switch (checkedId) {
            case R.id.type_satellite:
                mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
                break;
            case R.id.type_normal:
            default:
                mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
                break;
        }
    }
 
    /** OnMarkerClickListener Methods */
 
    @Override
    public boolean onMarkerClick(Marker marker) {
        // Return true to disable auto-center and info pop-up
        return false;
    }
 
    /** OnMarkerDragListener Methods */
 
    @Override
    public void onMarkerDrag(Marker marker) {
        // Do something while the marker is moving
    }
 
    @Override
    public void onMarkerDragEnd(Marker marker) {
        Log.i("MarkerTest", "Drag " + marker.getTitle()
                + " to " + marker.getPosition());
    }
 
    @Override
    public void onMarkerDragStart(Marker marker) {
        Log.d("MarkerTest", "Drag " + marker.getTitle()
                + " from " + marker.getPosition());
    }
    
    /** OnInfoWindowClickListener Methods */
 
    @Override
    public void onInfoWindowClick(Marker marker) {
        // Act on the event, here we just close the window
        marker.hideInfoWindow();
    }
 
    /** InfoWindowAdapter Methods */
 
    /*
     * Return a content view to be placed inside a standard
     * info window. Only called if getInfoWindow() returns null.
     */
    @Override
    public View getInfoContents(Marker marker) {
        return null;
    }
 
    /*
     * Return the entire info window to be displayed.
     * Returning null here also will show default info window.
     */
    @Override
    public View getInfoWindow(Marker marker) {
        return null;
    }
 
    /*
     * Private helper method to construct the content view
     */
    private View createInfoView(Marker marker) {
        // We have no parent for layout, so pass null
        View content = getLayoutInflater().inflate(
                R.layout.info_window, null);
        ImageView image = (ImageView) content
                .findViewById(R.id.image);
        TextView text = (TextView) content
                .findViewById(R.id.text);
 
        image.setImageResource(R.drawable.ic_launcher);
        text.setText(marker.getTitle());
 
        return content;
    }
}

Disclaimer  We have not visited the locations on this map to know if they are actually restaurants, or if their customer ratings qualify them for the subtitles we’ve placed here!

We’ve added some new listener interfaces to our activity, which is now set up to monitor for click-and-drag events on each Marker, as well as click events on the pop-up info window shown from a Marker tap. Additionally, we have implemented InfoWindowAdapter, which will allow us to customize the pop-up windows eventually, but let’s table that for now.

Markers are added to the map by passing a MarkerOptions instance into GoogleMap.addMarker(). MarkerOptions works like a builder, in that you can simply chain all the information you want to apply right off the constructor (which is what we have done). Basic information such as the marker location, display icon, and title are set here. You will also find additional options available for modifying the marker display, such as alpha, rotation, and anchor point. We’ve chosen to add a marker at Google HQ in Mountain View, and two others nearby.

There are a host of supported methods for creating a Marker icon. These are applied using a BitmapDescriptor object, and BitmapDescriptorFactory provides methods for creating all of them. For two of our elements, we have chosen defaultMarker(), which creates a standard Google pin to display. We can also pass in one of several constants to control the display color of the pin.

The marker at Google HQ has been customized to display as an icon we have in our application resources using fromResource(). You may also apply images that may be in our assets directory with a separate factory method. Additionally, we have set this marker to be draggable by the user. This means if the user were to long-press on this icon, it would be picked up from its current location and they could drag and drop the pin anywhere they like somewhere else on the map. The OnMarkerDragListener we implemented provides callbacks as to where the marker is being placed.

If the user taps one of the markers, the standard info window will show above the icon. That window will show the title and snippet applied to that marker. We have implemented an OnInfoWindowClickListener that closes this window when it is tapped, which is not the default behavior.

Note we do not need to implement OnMarkerClickListener in order to get this described behavior; but if we want to override it, we will. By default, the info window will display and the map will center on a selected marker. If we return true from onMarkerClick(), we can disable this and provide our own behavior.

When run, this activity produces the display shown in Figure 5-2.

9781430263227_Fig05-02.jpg

Figure 5-2. Map with ItemizedOverlay

Customizing the Info Window

To see how we can customize the info window that pops up on a marker tap, let’s add some custom UI for the window (see Listings 5-7 and 5-8) and modify the InfoWindowAdapter methods implemented in our activity to look like Listing 5-9.

Listing 5-7. res/layout/info_window.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical" >
    <ImageView
        android:id="@+id/image"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_gravity="center_horizontal"
        android:scaleType="fitCenter" />
    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

Listing 5-8. res/drawable/background.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:radius="10dp"/>
    <solid
        android:color="#CCC"/>
    <padding
        android:left="10dp"
        android:right="10dp"
        android:top="10dp"
        android:bottom="10dp"/>
</shape>

Listing 5-9. InfoWindowAdapter Methods

/*
 * Return a content view to be placed inside a standard info
 * window. Only called if getInfoWindow() returns null.
 */
@Override
public View getInfoContents(Marker marker) {
    //Try returning createInfoView() here instead
    return null;
}
 
/*
 * Return the entire info window to be displayed.
 */
@Override
public View getInfoWindow(Marker marker) {
    View content = createInfoView(marker);
    content.setBackgroundResource(R.drawable.background);
    return content;
}
 
/*
 * Private helper method to construct the content view
 */
private View createInfoView(Marker marker) {
    // We have no parent for layout, so pass null
    View content = getLayoutInflater().inflate(
            R.layout.info_window, null);
    ImageView image = (ImageView) content
            .findViewById(R.id.image);
    TextView text = (TextView) content
            .findViewById(R.id.text);
 
    image.setImageResource(R.drawable.ic_launcher);
    text.setText(marker.getTitle());
 
    return content;
}

By returning a valid View from getInfoContents(), the view will be used as the content inside the standard window background display. Returning the same View from getInfoWindow()will display it as a fully custom window with no standard components. We have abstracted the creation of our pop-up into a helper method so you can easily try it both ways.

Working with Shapes

Let’s talk about adding shape elements to a map. In the next example, we’ve created a custom class called ShapeAdapter that creates and adds circular or rectangular shapes on the map to describe map regions. It also uses the OnMapClickListener of GoogleMap to validate when a user has tapped a certain region to select it. Listing 5-10 shows the adapter code.

Listing 5-10. ShapeAdapter to Map Shapes

public class ShapeAdapter implements OnMapClickListener {
 
    private static final float STROKE_SELECTED = 6.0f;
    private static final float STROKE_NORMAL = 2.0f;
    /* Colors for the drawn regions */
    private static final int COLOR_STROKE = Color.RED;
    private static final int COLOR_FILL =
            Color.argb(127, 0, 0, 255);
    
    /*
     * External interface to notify listeners of a change in
     * the selected region based on user taps
     */
    public interface OnRegionSelectedListener {
        //User selected one of our tracked regions
        public void onRegionSelected(Region selectedRegion);
        //User selected an area where we have no regions
        public void onNoRegionSelected();
    }
    
    /*
     * Base definition of an interactive region on the map.
     * Defines methods to change display and check user taps
     */
    public static abstract class Region {
        private String mRegionName;
        public Region(String regionName) {
            mRegionName = regionName;
        }
        
        public String getName() {
            return mRegionName;
        }
        //Check if a location is inside this region
        public abstract boolean hitTest(LatLng point);
        //Change display of the region based on selection
        public abstract void setSelected(boolean isSelected);
    }
    
    /*
     * Implementation of a region drawn as a circle
     */
    private static class CircleRegion extends Region {
        private Circle mCircle;
        
        public CircleRegion(String name, Circle circle) {
            super(name);
            mCircle = circle;
        }
        
        @Override
        public boolean hitTest(LatLng point) {
            final LatLng center = mCircle.getCenter();
            float[] result = new float[1];
            Location.distanceBetween(center.latitude,
                    center.longitude,
                    point.latitude,
                    point.longitude,
                    result);
            
            return (result[0] < mCircle.getRadius());
        }
 
        @Override
        public void setSelected(boolean isSelected) {
            mCircle.setStrokeWidth(isSelected ?
                    STROKE_SELECTED : STROKE_NORMAL);
        }
        
    }
    
    /*
     * Implementation of a region drawn as a rectangle
     */
    private static class RectRegion extends Region {
        private Polygon mRect;
        private LatLngBounds mRectBounds;
        
        public RectRegion(String name, Polygon rect,
                LatLng southwest, LatLng northeast) {
            super(name);
            mRect = rect;
            mRectBounds = new LatLngBounds(southwest, northeast);
        }
 
        @Override
        public boolean hitTest(LatLng point) {
            return mRectBounds.contains(point);
        }
 
        @Override
        public void setSelected(boolean isSelected) {
            mRect.setStrokeWidth(isSelected ?
                    STROKE_SELECTED : STROKE_NORMAL);
        }
    }
    
    private GoogleMap mMap;
    
    private OnRegionSelectedListener mRegionSelectedListener;
    private ArrayList<Region> mRegions;
    private Region mCurrentRegion;
    
    public ShapeAdapter(GoogleMap map) {
        //Internally track regions for selection validation
        mRegions = new ArrayList<Region>();
        
        mMap = map;
        mMap.setOnMapClickListener(this);
    }
 
    public void setOnRegionSelectedListener(
            OnRegionSelectedListener listener) {
        mRegionSelectedListener = listener;
    }
    
    /*
     * Construct and add a new circular region around the
     * given point.
     */
    public void addCircularRegion(String name, LatLng center,
            double radius) {
        CircleOptions options = new CircleOptions()
                .center(center)
                .radius(radius);
        //Set display properties of the shape
        options
            .strokeWidth(STROKE_NORMAL)
            .strokeColor(COLOR_STROKE)
            .fillColor(COLOR_FILL);
        
        Circle c = mMap.addCircle(options);
        mRegions.add(new CircleRegion(name, c));
    }
    
    /*
     * Construct and add a new rectangular region with the
     * given boundaries.
     */
    public void addRectangularRegion(String name,
            LatLng southwest, LatLng northeast) {
        PolygonOptions options = new PolygonOptions().add(
                new LatLng(southwest.latitude,
                        southwest.longitude),
                new LatLng(southwest.latitude,
                        northeast.longitude),
                new LatLng(northeast.latitude,
                        northeast.longitude),
                new LatLng(northeast.latitude,
                        southwest.longitude));
 
        //Set display properties of the shape
        options
            .strokeWidth(STROKE_NORMAL)
            .strokeColor(COLOR_STROKE)
            .fillColor(COLOR_FILL);
 
        
        Polygon p = mMap.addPolygon(options);
        mRegions.add(new RectRegion(name, p,
                southwest, northeast));
    }
    
    /*
     * Handle incoming tap events from the map object.
     * Determine which region element may have been selected.
     * If regions overlap at this point, the first added will
     * be selected.
     */
    @Override
    public void onMapClick(LatLng point) {
        Region newSelection = null;
        //Find and select the tapped region
        for (Region region : mRegions) {
            if (region.hitTest(point) && newSelection == null) {
                region.setSelected(true);
                newSelection = region;
            } else {
                region.setSelected(false);
            }
        }
 
        if (mCurrentRegion != newSelection) {
            //Notify and update the change
            if (newSelection != null
                    && mRegionSelectedListener != null) {
                mRegionSelectedListener
                        .onRegionSelected(newSelection);
            } else if (mRegionSelectedListener != null) {
                mRegionSelectedListener.onNoRegionSelected();
            }
            
            mCurrentRegion = newSelection;
        }
    }
}

This class defines an abstract type called Region that we can use to define common patterns between our shape types. Primarily, each region must define the logic for whether a map location is inside the given region, and what to do when that region is selected. We then define implementations of this for a Circle shape and a Polygon, which we will use to draw a rectangle. A center point and a radius define the circular region, while the rectangular region is defined by its southwest and northeast point. We construct the rectangle by constructing a Polygon out of a list of the four corner points that make up the shape.

Tap events will come in through the onMapClick() method of the listener interface, and the Maps library gives us the tap location as a LatLng location. We can validate that these events are inside a circular region simply enough by checking whether the distance between the center and the tap is larger than the radius. Location has a convenience method for calculating direct distance between two map points. For a rectangular region, we use the LatLngBounds class that is part of the Maps library because it can directly validate whether a given point is inside or outside our shape.

For each tap event, we iterate over our list of regions to find the first one that considers this location a hit. If we find no regions, the selected region is set to null. We then determine whether the selection has changed, and call back one of the methods on our custom OnRegionSelectedListener interface that higher-level objects can use to be notified of these events.

Listing 5-11 shows how we can use this adapter inside an activity.

Listing 5-11. Activity Integrating ShapeAdapter

public class ShapeMapActivity extends FragmentActivity implements
        RadioGroup.OnCheckedChangeListener,
        ShapeAdapter.OnRegionSelectedListener {
 
    private SupportMapFragment mMapFragment;
    private GoogleMap mMap;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        // Check if Google Play Services is up-to-date.
        switch (GooglePlayServicesUtil
                .isGooglePlayServicesAvailable(this)) {
            case ConnectionResult.SUCCESS:
                // Do nothing, move on
                break;
            case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
                Toast.makeText(this,
                        "Maps service requires an update, "
                        + "please open Google Play.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
            default:
                Toast.makeText(this,
                        "Maps are not available on this device.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
        }
 
        mMapFragment =
                (SupportMapFragment) getSupportFragmentManager()
                        .findFragmentById(R.id.map);
        mMap = mMapFragment.getMap();
 
        ShapeAdapter adapter = new ShapeAdapter(mMap);
        adapter.setOnRegionSelectedListener(this);
 
        //Add our previous markers as shape regions
        adapter.addRectangularRegion("Google HQ",
                new LatLng(37.4168, -122.0890),
                new LatLng(37.4268, -122.0790));
        adapter.addCircularRegion("Neighbor #1",
                new LatLng(37.4118, -122.0740), 400);
        adapter.addCircularRegion("Neighbor #2",
                new LatLng(37.4318, -122.0940), 400);
        
        //Center and zoom map simultaneously
        LatLng mapCenter = new LatLng(37.4218,  -122.0840);
        CameraUpdate newCamera =
                CameraUpdateFactory.newLatLngZoom(mapCenter, 13);
        mMap.moveCamera(newCamera);
        
        //Wire up the map type selector UI
        RadioGroup typeSelect =
                (RadioGroup) findViewById(R.id.group_maptype);
        typeSelect.setOnCheckedChangeListener(this);
        typeSelect.check(R.id.type_normal);
    }
 
    /** OnCheckedChangeListener Methods */
    
    @Override
    public void onCheckedChanged(RadioGroup group,
            int checkedId) {
        switch (checkedId) {
            case R.id.type_satellite:
                mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
                break;
            case R.id.type_normal:
            default:
                mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
                break;
        }
    }
 
    /** OnRegionSelectedListener Methods */
 
    @Override
    public void onRegionSelected(Region selectedRegion) {
        Toast.makeText(this, selectedRegion.getName(),
                Toast.LENGTH_SHORT).show();
    }
 
    @Override
    public void onNoRegionSelected() {
        Toast.makeText(this, "No Region",
                Toast.LENGTH_SHORT).show();
    }
}

Here we have added the same locations from our previous example, but this time as shape regions using our new ShapeAdapter. Google HQ is added as a rectangular region, and the other two as circles. When the user makes a selection change affecting any of these regions, either of the methods onRegionSelected() or onNoRegionSelected() will be called and a message displayed. Figure 5-3 shows our region selector in action.

9781430263227_Fig05-03.jpg

Figure 5-3. Map overlay with custom markers

5-4. Monitoring Location Regions

Problem

You need your application to provide contextual information to your users when they enter or exit specific location areas.

Solution

(API Level 8)

Use the geofencing features available as part of Google Play Services. With these features, your application can define circular areas around a particular point for which you want to receive callbacks when the user moves into or out of that region. Your application can create multiple Geofence instances that are either tracked indefinitely, or automatically removed for you after an expiration time.

Using region-based monitoring of a user’s location can be a significantly more power-efficient method of tracking that user’s arrival at a location that you find important. Allowing the services framework to track location and call you back in this manner will often result in much better battery life than your application continuously tracking user location to find out when they reach a given destination.

Important  The geofencing features described here are part of the Google Play Services library; they are not part of the native SDK at any platform level. However, any application targeting API Level 8 or later and devices inside the Google Play ecosystem can use the mapping library. For more information on including Google Play Services in your project, reference our guide in Chapter 1.

How It Works

We are going to create an application that consists of a simple activity to allow the user to set a geofence around their current location, and then explicitly start or stop monitoring. Once monitoring is enabled, a background service will be activated to respond to events related to the user’s location transitioning into or out of the geofence area. The service component allows us to respond to these events without the need for our application’s UI to be in the foreground.

Important  Because we are accessing the user’s location in this example, we need to request the android.permission.ACCESS_FINE_LOCATION permission in our AndroidManifest.xml.

Let’s start with Listing 5-12, which describes the layout of the activity.

Listing 5-12. res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <TextView
        android:id="@+id/status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <SeekBar
        android:id="@+id/radius"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="1000"/>
    <TextView
        android:id="@+id/radius_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Set Geofence at My Location"
        android:onClick="onSetGeofenceClick" />
 
    <!-- Spacer -->
    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Monitoring"
        android:onClick="onStartMonitorClick" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Monitoring"
        android:onClick="onStopMonitorClick" />
</LinearLayout>

The layout contains a SeekBar that enables the user to slide a finger to select the desired radius value. The user can lock in the new geofence by tapping the uppermost button, and start or stop monitoring by using the buttons at the bottom. Figure 5-4 illustrates what the UI should look like.

9781430263227_Fig05-04.jpg

Figure 5-4. RegionMonitor’s control activity

Listing 5-13 shows the activity code to manage the geofence monitoring.

Listing 5-13. Activity to Set a Geofence

public class MainActivity extends Activity implements
        OnSeekBarChangeListener,
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationClient.OnAddGeofencesResultListener,
        LocationClient.OnRemoveGeofencesResultListener {
    private static final String TAG = "RegionMonitorActivity";
 
    //Unique identifier for our single geofence
    private static final String FENCE_ID =
            "com.androidrecipes.FENCE";
    
    private LocationClient mLocationClient;
    private SeekBar mRadiusSlider;
    private TextView mStatusText, mRadiusText;
    
    private Geofence mCurrentFence;
    private Intent mServiceIntent;
    private PendingIntent mCallbackIntent;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //Wire up the UI connections
        mStatusText = (TextView) findViewById(R.id.status);
        mRadiusText = (TextView) findViewById(R.id.radius_text);
        mRadiusSlider = (SeekBar) findViewById(R.id.radius);
        mRadiusSlider.setOnSeekBarChangeListener(this);
        updateRadiusDisplay();
        
        //Check if Google Play Services is up-to-date.
        switch (GooglePlayServicesUtil
                .isGooglePlayServicesAvailable(this)) {
            case ConnectionResult.SUCCESS:
                //Do nothing, move on
                break;
            case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
                Toast.makeText(this,
                        "Geofencing service requires an update,"
                        + " please open Google Play.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
            default:
                Toast.makeText(this,
                        "Geofencing service is not available.",
                        Toast.LENGTH_SHORT).show();
                finish();
                return;
        }
        //Create a client for Google Services
        mLocationClient = new LocationClient(this, this, this);
        //Create an Intent to trigger our service
        mServiceIntent = new Intent(this,
                RegionMonitorService.class);
        //Create a PendingIntent for Google Services callbacks
        mCallbackIntent = PendingIntent.getService(this, 0,
                mServiceIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        //Connect to all services
        if (!mLocationClient.isConnected()
                && !mLocationClient.isConnecting()) {
            mLocationClient.connect();
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        //Disconnect when not in the foreground
        mLocationClient.disconnect();
    }
    
    public void onSetGeofenceClick(View v) {
        //Obtain the last location from services and radius
        // from the UI
        Location current = mLocationClient.getLastLocation();
        int radius = mRadiusSlider.getProgress();
        
        //Create a new Geofence using the Builder
        Geofence.Builder builder = new Geofence.Builder();
        mCurrentFence = builder
            //Unique to this geofence
            .setRequestId(FENCE_ID)
            //Size and location
            .setCircularRegion(
                current.getLatitude(),
                current.getLongitude(),
                radius)
            //Events both in and out of the fence
            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER
                    | Geofence.GEOFENCE_TRANSITION_EXIT)
            //Keep alive
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
            .build();
        
        mStatusText.setText(String.format(
                "Geofence set at %.3f, %.3f",
                current.getLatitude(),
                current.getLongitude()) );
    }
    
    public void onStartMonitorClick(View v) {
        if (mCurrentFence == null) {
            Toast.makeText(this, "Geofence Not Yet Set",
                    Toast.LENGTH_SHORT).show();
            return;
        }
        
        //Add the fence to start tracking, the PendingIntent will
        // be triggered with new updates
        ArrayList<Geofence> geofences = new ArrayList<Geofence>();
        geofences.add(mCurrentFence);
        mLocationClient.addGeofences(geofences,
                mCallbackIntent, this);
    }
    
    public void onStopMonitorClick(View v) {
        //Remove to stop tracking
        mLocationClient.removeGeofences(mCallbackIntent, this);
    }
    
    /** SeekBar Callbacks */
    
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress,
            boolean fromUser) {
        updateRadiusDisplay();
    }
    
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) { }
    
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) { }
    
    private void updateRadiusDisplay() {
        mRadiusText.setText(mRadiusSlider.getProgress()
                + " meters");
    }
    
    /** Google Services Connection Callbacks */
    
    @Override
    public void onConnected(Bundle connectionHint) {
        Log.v(TAG, "Google Services Connected");
    }
    
    @Override
    public void onDisconnected() {
        Log.w(TAG, "Google Services Disconnected");
    }
    
    @Override
    public void onConnectionFailed(ConnectionResult result) {
        Log.w(TAG, "Google Services Connection Failure");
    }
    
    /** LocationClient Callbacks */
    
    /*
     * Called when the asynchronous geofence add is complete.
     * When this happens, we start our monitoring service.
     */
    @Override
    public void onAddGeofencesResult(int statusCode,
            String[] geofenceRequestIds) {
        if (statusCode == LocationStatusCodes.SUCCESS) {
            Toast.makeText(this,
                    "Geofence Added Successfully",
                    Toast.LENGTH_SHORT).show();
        }
        
        Intent startIntent = new Intent(mServiceIntent);
        startIntent.setAction(RegionMonitorService.ACTION_INIT);
        startService(mServiceIntent);
    }
 
    /*
     * Called when the asynchronous geofence remove is complete.
     * The version called depends on whether you requested the
     * removal via PendingIntent or request Id.
     * When this happens, we stop our monitoring service.
     */
    @Override
    public void onRemoveGeofencesByPendingIntentResult(
            int statusCode, PendingIntent pendingIntent) {
        if (statusCode == LocationStatusCodes.SUCCESS) {
            Toast.makeText(this, "Geofence Removed Successfully",
                    Toast.LENGTH_SHORT).show();
        }
        
        stopService(mServiceIntent);
    }
    
    @Override
    public void onRemoveGeofencesByRequestIdsResult(
            int statusCode, String[] geofenceRequestIds) {
        if (statusCode == LocationStatusCodes.SUCCESS) {
            Toast.makeText(this, "Geofence Removed Successfully",
                    Toast.LENGTH_SHORT).show();
        }
        
        stopService(mServiceIntent);
    }
}

Our first order of business after the activity has been created is to verify that Google Play Services exists and is up-to-date. If not, we need to encourage the user to visit Google Play to trigger the latest automatic update.

With that out of the way, we make a connection to the location services through a LocationClient instance. We want to stay connected to this only while in the foreground, so the connection calls are balanced between onResume() and onPause(). This connection is asynchronous, so we must wait for the onConnected() method before doing anything further. In our case, we need to access the LocationClient only when the user presses a button, so there is nothing of specific interest to do in this method.

Tip  Asynchronous doesn’t have to mean slow. Just because a method call is asynchronous doesn’t mean we should expect it to take a long time. It simply means we cannot access the object immediately after the function returns. In most cases, these callbacks are still triggered long before the activity is fully visible.

Once the user has selected the desired radius and taps the Set Geofence button, we obtain the last-known location from the LocationClient and the selected radius to build our geofence. Geofence instances are created using the Geofence.Builder, which allows us to set the location of the geofence, a unique identifier, and any additional properties we may need.

With setTransitionTypes(), we control which transitions generate notifications. There are two possible values for transitions: GEOFENCE_TRANSITION_ENTER and GEOFENCE_TRANSITION_EXIT. You may request callbacks on one or both events; we’ve chosen both.

The expiration time, when positive, represents a time in the future from when the Geofence is added that it should be automatically removed. Setting the value to NEVER_EXPIRE allows us to track this region indefinitely until we remove it manually.

At any point in the future when the user taps the Start Monitoring button, we will request updates for this region by calling LocationClient.addGeofences() with both the Geofence and a PendingIntent that the framework will fire for each new monitoring event. Notice in our case that PendingIntent points to a service. This request is also asynchronous, and we will receive a callback via onAddGeofencesResult() when the operation is finished. At this point, a start command is sent to our background service, which we will discuss in more detail shortly.

Finally, when the user taps the Stop Monitoring button, the geofence will be removed and new updates will cease. We reference which element(s) to remove by using the same PendingIntent that was passed to the original request. Geofences can also be removed by using the unique identifier they were originally built with. Once the asynchronous remove is complete, a stop command is sent to our background service.

In both the start and stop cases, we are sending an Intent to the service with a unique action string so the service can differentiate between these requests and the updates it will receive from location services. Listing 5-14 reveals this background service that we’ve been hearing so much about.

Listing 5-14. Region Monitor Service

public class RegionMonitorService extends Service {
    private static final String TAG = "RegionMonitorService";
    
    private static final int NOTE_ID = 100;
    //Unique action to identify start requests vs. events
    public static final String ACTION_INIT =
            "com.androidrecipes.regionmonitor.ACTION_INIT";
    
    private NotificationManager mNoteManager;
    
    @Override
    public void onCreate() {
        super.onCreate();
        mNoteManager = (NotificationManager) getSystemService(
                NOTIFICATION_SERVICE);
        //Post a system notification when the service starts
        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.drawable.ic_launcher);
        builder.setContentTitle("Geofence Service");
        builder.setContentText("Waiting for transition...");
        builder.setOngoing(true);
        
        Notification note = builder.build();
        mNoteManager.notify(NOTE_ID, note);
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags,
                int startId) {
        //Nothing to do yet, just starting the service
        if (ACTION_INIT.equals(intent.getAction())) {
            //We don't care if this service dies unexpectedly
            return START_NOT_STICKY;
        }
        
        if (LocationClient.hasError(intent)) {
            //Log any errors
            Log.w(TAG, "Error monitoring region: "
                    + LocationClient.getErrorCode(intent));
        } else {
            //Update the ongoing notification from the new event
            NotificationCompat.Builder builder =
                    new NotificationCompat.Builder(this);
            builder.setSmallIcon(R.drawable.ic_launcher);
            builder.setDefaults(Notification.DEFAULT_SOUND
                    | Notification.DEFAULT_LIGHTS);
            builder.setOngoing(true);
            
            int transitionType =
                    LocationClient.getGeofenceTransition(intent);
            //Check whether we entered or exited the region
            if (transitionType ==
                    Geofence.GEOFENCE_TRANSITION_ENTER) {
                builder.setContentTitle("Geofence Transition");
                builder.setContentText("Entered your Geofence");
            } else if (transitionType ==
                    Geofence.GEOFENCE_TRANSITION_EXIT) {
                builder.setContentTitle("Geofence Transition");
                builder.setContentText("Exited your Geofence");
            }
            
            Notification note = builder.build();
            mNoteManager.notify(NOTE_ID, note);
        }
        
        //We don't care if this service dies unexpectedly
        return START_NOT_STICKY;
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        //When the service dies, cancel our ongoing notification
        mNoteManager.cancel(NOTE_ID);
    }
    
    /* We are not binding to this service */
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

The primary role of this service is to receive updates from the location services about our monitored region and post them to a notification in the status bar so the user can see the change. We will talk in more detail about how notifications work and how to create them in a later chapter.

When the service is first created (which will happen when the start command is sent after our button press), an initial notification is created and posted to the status bar. This will be followed by the first onStartCommand(), where we find our unique action string and do nothing further.

Relatively immediately after this occurs, the first region monitoring event will come into this service, calling onStartCommand() again. This first event is a transition that indicates the initial state of the device location with respect to the Geofence. In this case, we check to see whether the Intent contains an error message, and if it is a successful tracking event, we construct an updated notification based on the transition information contained within and post the update to the status bar.

This process will repeat for each new event we receive while the region monitoring is active. When the user finally returns to our activity and presses Stop Monitoring, that stop command will cause onDestroy()to be called in the service. It is here that we remove the notification from the status bar to signify to the user that monitoring is no longer active.

Note  If you have activated multiple Geofence instances by using the same PendingIntent, you can use the additional method LocationClient.getTriggeringGeofences() to determine which regions were part of any given event.

5-5. Capturing Images and Video

Problem

Your application needs to use the device’s camera in order to capture media, whether it be still images or short video clips.

Solution

(API Level 3)

Send an Intent to Android to transfer control to the Camera application and to return the image the user captured. Android does contain APIs for directly accessing the camera hardware, previewing, and taking snapshots or videos. However, if your only goal is to simply get the media content by using the camera with an interface the user is familiar with, there is no better solution than a handoff.

How It Works

Let’s take a look at how to use the Camera application to take both still images and video clips.

Image Capture

Let’s take a look at an example activity that will activate the Camera application when the Take a Picture button is pressed; you will receive the result of this operation as a Bitmap. See Listings 5-15 and 5-16.

Listing 5-15. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/capture"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take a Picture" />
    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerInside" />
</LinearLayout>

Listing 5-16. Activity to Capture an Image

public class MyActivity extends Activity {
 
    private static final int REQUEST_IMAGE = 100;
    
    Button captureButton;
    ImageView imageView;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);
        
        imageView = (ImageView)findViewById(R.id.image);
    }
    
    @Override
    protected void onActivityResult(int requestCode,
            int resultCode, Intent data) {
        if(requestCode == REQUEST_IMAGE
                && resultCode == Activity.RESULT_OK) {
            //Process and display the image
            Bitmap userImage =
                    (Bitmap)data.getExtras().get("data");
            imageView.setImageBitmap(userImage);
        }
    }
    
    private View.OnClickListener listener =
            new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                Intent intent =
                        new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                startActivityForResult(intent, REQUEST_IMAGE);
            } catch (ActivityNotFoundException e) {
                //Handle if no application exists
            }
        }
    };
}

In this example, we construct an Intent to activate the Camera application and capture an image. While it is unlikely, we want to be prepared for the case that a Camera application does not exist on the device. In this case, the call to startActivity() will throw an ActivityNotFoundException, so we have wrapped the call in a try block in order to handle this case gracefully.

Tip  You can also query whether camera hardware is present with the PackageManager.hasSystemFeature() method, passing in PackageManager.FEATURE_CAMERA as the parameter.

This method captures the image and returns a scaled-down Bitmap as an extra in the data field. If you need to capture the full-sized image, insert a Uri for the image destination into the MediaStore.EXTRA_OUTPUT field of the Intent before starting the capture, and the image will be saved at that location. See Listing 5-17.

Listing 5-17. Full-Size Image Capture to File

public class MyActivity extends Activity {
    
    private static final int REQUEST_IMAGE = 100;
    
    Button captureButton;
    ImageView imageView;
    File destination;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);
        
        imageView = (ImageView)findViewById(R.id.image);
        
        destination = new File(Environment
                .getExternalStorageDirectory(), "image.jpg");
    }
    
    @Override
    protected void onActivityResult(int requestCode,
            int resultCode, Intent data) {
        if(requestCode == REQUEST_IMAGE
                && resultCode == Activity.RESULT_OK) {
            try {
                FileInputStream in =
                        new FileInputStream(destination);
                BitmapFactory.Options options =
                        new BitmapFactory.Options();
                options.inSampleSize = 10; //Downsample by 10x
 
                Bitmap userImage = BitmapFactory
                        .decodeStream(in, null, options);
                imageView.setImageBitmap(userImage);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    private View.OnClickListener listener =
            new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                Intent intent =
                        new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                //Add extra to save full-image somewhere
                intent.putExtra(MediaStore.EXTRA_OUTPUT,
                        Uri.fromFile(destination));
                startActivityForResult(intent, REQUEST_IMAGE);
            } catch (ActivityNotFoundException e) {
                //Handle if no application exists
            }
        }
    };
}

This method will instruct the Camera application to store the image elsewhere (in this case, on the device’s SD card as image.jpg), and the result will not be scaled down. When going to retrieve the image after the operation returns, we now go directly to the file location where we told the camera to store the image.

Tip  The documentation states that only one image output should be expected. If no Uri exists, a small image is returned as data. Otherwise, the image is saved to the Uri location. You should not expect to receive both, even if some devices in the market behave this way.

Using BitmapFactory.Options, however, we do still scale the image down prior to displaying to the screen to avoid loading the full-size Bitmap into memory at once. Also note that this example chose a file location that was on the device’s external storage, which requires the android.permission.WRITE_EXTERNAL_STORAGE permission to be declared in API Levels 4 and above. If your final solution writes the file elsewhere, this may not be necessary.

Video Capture

Capturing video clips by using this method is just as straightforward, although the results produced are slightly different. There is no case under which the actual video-clip data is returned directly in the Intent extras, and it is always saved to a destination file location. The following two parameters may be passed along as extras:

  • MediaStore.EXTRA_VIDEO_QUALITY: Integer value to describe the quality level used to capture the video. Allowed values are 0 for low quality and 1 for high quality.
  • MediaStore.EXTRA_OUTPUT: Uri destination of where to save the video content. If this is not present, the video will be saved in a standard location for the device.

When the video recording is complete, the actual location where the data was saved is returned as a Uriin the data field of the result Intent. Let’s take a look at a similar example that allows the user to record and save their videos and then display the saved location back to the screen. See Listings 5-18 and 5-19.

Listing 5-18. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/capture"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take a Video" />
    <TextView
        android:id="@+id/file"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Listing 5-19. Activity to Capture a Video Clip

public class MyActivity extends Activity {
    
    private static final int REQUEST_VIDEO = 100;
    
    Button captureButton;
    TextView text;
    File destination;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        captureButton = (Button)findViewById(R.id.capture);
        captureButton.setOnClickListener(listener);
        
        text = (TextView)findViewById(R.id.file);
        
        destination = new File(Environment
                .getExternalStorageDirectory(), "myVideo");
    }
    
    @Override
    protected void onActivityResult(int requestCode,
            int resultCode, Intent data) {
        if(requestCode == REQUEST_VIDEO
                && resultCode == Activity.RESULT_OK) {
            String location = data.getData().toString();
            text.setText(location);
        }
    }
    
    private View.OnClickListener listener =
            new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                Intent intent =
                        new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
                //Add (optional) extra to save video to our file
                intent.putExtra(MediaStore.EXTRA_OUTPUT,
                        Uri.fromFile(destination));
                //Optional extra to set video quality
                intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
                startActivityForResult(intent, REQUEST_VIDEO);
 
            } catch (ActivityNotFoundException e) {
                //Handle if no application exists
            }
        }
    };
}

This example, like the previous example saving an image, puts the recorded video on the device’s SD card (which requires the android.permission.WRITE_EXTERNAL_STORAGE permission for API Levels 4+). To initiate the process, we send an Intent with the MediaStore.ACTION_VIDEO_CAPTUREaction string to the system. Android will launch the default Camera application to handle recording the video and return with an OK result when recording is complete. We retrieve the location where the data was stored as a Uri by calling Intent.getData() in the onActivityResult() callback method, and then display that location to the user.

This example requests explicitly that the video be shot using the low-quality setting, but this parameter is optional. If MediaStore.EXTRA_VIDEO_QUALITY is not present in the request Intent, the device will usually choose to shoot using high quality.

In cases where MediaStore.EXTRA_OUTPUT is provided, the Uri returned should match the location you requested, unless an error occurs that keeps the application from writing to that location. If this parameter is not provided, the returned value will be a content:// Uri to retrieve the media from the system’s MediaStore Content Provider.

Later, in Recipe 5-10, we will look at practical ways to play this media back in your application.

5-6. Making a Custom Camera Overlay

Problem

Many applications need more-direct access to the camera, either for the purposes of overlaying a custom user interface (UI) for controls or displaying metadata about what is visible through information based on location and direction sensors (augmented reality).

Solution

(API Level 5)

Attach directly to the camera hardware in a custom activity. Android provides APIs to directly access the device’s camera for the purposes of obtaining the preview feed and taking photos. We can access these when the needs of the application grow beyond simply snapping and returning a photo for display.

Note  Because we are taking a more direct approach to the camera here, the android.permission.CAMERA permission must be declared in the manifest.

How It Works

We start by creating a SurfaceView, a dedicated view for live drawing where we will attach the camera’s preview stream. This provides us with a live preview inside a view that we can lay out any way we choose inside an activity. From there, it’s simply a matter of adding other views and controls that suit the context of the application. Let’s take a look at the code (see Listings 5-20 and 5-21).

Note  The Camera class used here is android.hardware.Camera, not to be confused with android.graphics.Camera. Ensure that you have imported the correct reference within your application.

Listing 5-20. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <SurfaceView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

Listing 5-21. Activity Displaying Live Camera Preview

import android.hardware.Camera;
 
public class PreviewActivity extends Activity implements
        SurfaceHolder.Callback {
 
    Camera mCamera;
    SurfaceView mPreview;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mPreview = (SurfaceView)findViewById(R.id.preview);
        mPreview.getHolder().addCallback(this);
        //Needed for support prior to Android 3.0
        mPreview.getHolder()
                .setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        
        mCamera = Camera.open();
    }
    
    @Override
    public void onPause() {
        super.onPause();
        mCamera.stopPreview();
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        mCamera.release();
    }
 
    //Surface Callback Methods
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format,
            int width, int height) {
        Camera.Parameters params = mCamera.getParameters();
        //Get the device's supported sizes and pick the first,
        // which is the largest
        List<Camera.Size> sizes =
                params.getSupportedPreviewSizes();
        Camera.Size selected = sizes.get(0);
        params.setPreviewSize(selected.width,selected.height);
        mCamera.setParameters(params);
 
        mCamera.startPreview();
    }
 
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera.setPreviewDisplay(mPreview.getHolder());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) { }
}

Note  If you are testing on an emulator, there may not be a camera to preview. Newer versions of the SDK have started to use cameras built into some host machines, but this is not universal. Where a camera is unavailable, the emulator displays a fake preview that looks slightly different depending on the version you are running. To verify that this code is working properly, open the Camera application on your specific emulator and take note of what the preview looks like. The same display should appear in this sample. It is always best to test code that integrates with device hardware on an actual device.

In the example, we create a SurfaceView that fills the window and tells it that our activity is to be notified of all the SurfaceHolder callbacks. The camera cannot begin displaying preview information on the surface until it is fully initialized, so we wait until surfaceCreated() gets called to attach the SurfaceHolder of our view to the Camera instance. Similarly, we wait to size the preview and start drawing until the surface has been given its size, which occurs when surfaceChanged() is called.

The camera hardware resources are opened and claimed for this application by calling Camera.open(). There is an alternate version of this method introduced in Android 2.3 (API Level 9) that takes an integer parameter (valid values being from 0 to getNumberOfCameras()-1) to determine which camera you would like to access for devices that have more than one. On these devices, the version that takes no parameters will always default to the rear-facing camera.

Important  Some newer devices, such as Google’s Nexus 7 tablet, do not have a rear-facing camera, and so the old implementation of Camera.open() will return null. If you have a Camera application that supports older versions of Android, you will want to branch your code and use the newer API where available to get whatever camera the device has to offer.

Calling Parameters.getSupportedPreviewSizes() returns a list of all the sizes the device will accept, and they are typically ordered largest to smallest. In the example, we pick the first (and, thus, largest) preview resolution and use it to set the size.

Note  In versions earlier than 2.0 (API Level 5), it was acceptable to directly pass the height and width parameters from this method as to Parameters.setPreviewSize(); but in 2.0, and later, the camera will set its preview to only one of the supported resolutions of the device. Attempts otherwise will result in an exception.

Camera.startPreview()begins the live drawing of camera data on the surface. Notice that the preview always displays in a landscape orientation. Prior to Android 2.2 (API Level 8), there was no official way to adjust the rotation of the preview display. For that reason, it is recommended that an activity using the camera preview have its orientation fixed with android:screenOrientation="landscape" in the manifest to match if you must support devices running older versions.

The Camera service can be accessed by only one application at a time. For this reason, it is important that you call Camera.release()as soon as the camera is no longer needed. In the example, we no longer need the camera when the activity is finished, so this call takes place in onDestroy().

Changing Capture Orientation

(API Level 8)

Starting with Android 2.2, the ability to rotate the actual camera preview was added. Applications can now call Camera.setDisplayOrientation()to rotate the incoming data to match the orientation of their activity. Valid values are degrees of 0, 90, 180, and 270; 0 will map to the default landscape orientation. This method affects primarily how the preview data is drawn on the surface before the capture.

To rotate the output data from the camera, use the method setRotation() on Camera.Parameters. This method’s implementation depends on the device; it will either rotate the actual image output, update the EXIF data with a rotation parameter, or both.

Overlaying the Preview

We can now add on to the previous example any controls or views that are appropriate to display on top of the camera preview. Let’s modify the preview to include a Cancel button and a Snap Photo button. See Listings 5-22 and 5-23.

Listing 5-22. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <SurfaceView
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="100dip"
        android:layout_alignParentBottom="true"
        android:gravity="center_vertical"
        android:background="#A000">
        <Button
            android:layout_width="100dip"
            android:layout_height="wrap_content"
            android:text="Cancel"
            android:onClick="onCancelClick" />
        <Button
            android:layout_width="100dip"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="Snap Photo"
            android:onClick="onSnapClick" />
    </RelativeLayout>
</RelativeLayout>

Listing 5-23. Activity with Photo Controls Added

public class PreviewActivity extends Activity implements
      SurfaceHolder.Callback, Camera.ShutterCallback, Camera.PictureCallback {
 
    Camera mCamera;
    SurfaceView mPreview;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mPreview = (SurfaceView)findViewById(R.id.preview);
        mPreview.getHolder().addCallback(this);
        //Neede for support prior to Android 3.0
        mPreview.getHolder()
                .setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        
        mCamera = Camera.open();
    }
    
    @Override
    public void onPause() {
        super.onPause();
        mCamera.stopPreview();
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        mCamera.release();
        Log.d("CAMERA","Destroy");
    }
    
    public void onCancelClick(View v) {
        finish();
    }
    
    public void onSnapClick(View v) {
        //Snap a photo
        mCamera.takePicture(this, null, null, this);
    }
 
    //Camera Callback Methods
    @Override
    public void onShutter() {
        Toast.makeText(this, "Click!", Toast.LENGTH_SHORT).show();
    }
 
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        
        //Store the picture off somewhere
        //Here, we chose to save to internal storage
        try {
            FileOutputStream out =
                    openFileOutput("picture.jpg", Activity.MODE_PRIVATE);
            out.write(data);
            out.flush();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
 
        //Must restart preview
        camera.startPreview();
    }
    
    //Surface Callback Methods
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format,
            int width, int height) {
        Camera.Parameters params = mCamera.getParameters();
        List<Camera.Size> sizes = params.getSupportedPreviewSizes();
        Camera.Size selected = sizes.get(0);
        params.setPreviewSize(selected.width,selected.height);
        mCamera.setParameters(params);
        
        mCamera.setDisplayOrientation(90);
        mCamera.startPreview();
    }
 
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera.setPreviewDisplay(mPreview.getHolder());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) { }
}

Here we have added a simple, partially transparent overlay to include a pair of controls for camera operation. The action taken by Cancel is nothing to speak of; we simply finish the activity. However, Snap Photo introduces more of the Camera API in manually taking and returning a photo to the application. A user action will initiate the Camera.takePicture() method, which takes a series of callback pointers.

Notice that the activity in this example implements two more interfaces: Camera.ShutterCallbackand Camera.PictureCallback. The former is called as near as possible to the moment when the image is captured (when the “shutter” closes), while the latter can be called at multiple instances when different forms of the image are available.

The parameters of takePicture() are a single ShutterCallback and up to three PictureCallback instances. The PictureCallbacks will be called at the following times (in the order they appear as parameters):

  • After the image is captured with RAW image data. This may return null on devices with limited memory.
  • After the image is processed with scaled image data (known as the POSTVIEW image). This may return null on devices with limited memory.
  • After the image is compressed with JPEG image data.

This example cares to be notified only when the JPEG is ready. Consequently, that is also the last callback made and the point in time when the preview must be started back up again. If startPreview() is not called again after a picture is taken, then preview on the surface will remain frozen at the captured image.

Tip  If you would like to guarantee that your application is downloaded only on devices that have the appropriate hardware, you can use the market filter for the camera in your manifest with the following line: <uses-feature android:name="android.hardware.camera"/>.

..................Content has been hidden....................

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