Hardware Sensors - Professional Android 4 Application Development (2012)

Professional Android 4 Application Development (2012)

Chapter 12. Hardware Sensors

What's in this Chapter?

Using the Sensor Manager

Introducing the available sensor types

Finding a device's natural orientation

Remapping a device's orientation reference frame

Monitoring sensors and interpreting sensor values

Using sensors to monitor a device's movement and orientation

Using sensors to monitor a device's environment

Modern Android devices are much more than simple communications or web browsing platforms. They are now extra-sensory devices that use hardware sensors, including accelerometers, gyroscopes, and barometers, to provide a platform to extend your perceptions.

Sensors that detect physical and environmental properties offer an exciting avenue for innovations that enhance the user experience of mobile applications. The incorporation of an increasingly rich array of sensor hardware in modern devices provides new possibilities for user interaction and application development, including augmented reality, movement-based input, and environmental customizations.

In this chapter you'll be introduced to the sensors currently available in Android and how to use the Sensor Manager to monitor them.

You'll take a closer look at the accelerometer, orientation, and gyroscopic sensors, and use them to determine changes in the device orientation and acceleration, regardless of the natural orientation of the host device. This is particularly useful for creating motion-based user interfaces (UIs).

You'll also explore the environmental sensors, including how to use the barometer to detect the current altitude, the light Sensor to determine the level of cloud cover, and the temperature Sensor to measure the ambient temperature.

Finally, you'll learn about the virtual and composite Sensors, which amalgamate the output of several hardware sensors to provide smoother and more accurate results.

Using Sensors and the Sensor Manager

The Sensor Manager is used to manage the sensor hardware available on Android devices. Use getSystemService to return a reference to the Sensor Manager Service, as shown in the following snippet:

String service_name = Context.SENSOR_SERVICE;
SensorManager sensorManager = (SensorManager)getSystemService(service_name);

Rather than interacting with the sensor hardware directly, they are represented by Sensor objects that describe the properties of the hardware sensor they represent, including its type, name, manufacturer, and details on its accuracy and range.

The Sensor class includes a set of constants that describe which type of hardware sensor is being represented by a particular Sensor object. These constants take the form of Sensor.TYPE_<TYPE>. The following section describes each supported sensor type, after which you'll learn how to find and use these sensors.

Supported Android Sensors

The following sections describe each sensor type currently available. Note that the hardware available on the host device determines which of these sensors will be available to your application.

· Sensor.TYPE_AMBIENT_TEMPERATURE—Introduced in Android 4.0 (API level 14) to replace the ambiguous—and deprecated—Sensor.TYPE_TEMPERATURE. This is a thermometer that returns the temperature in degrees Celsius; the temperature returned will be the ambient room temperature.

· Sensor.TYPE_ACCELEROMETER—A three-axis accelerometer that returns the current acceleration along three axes in m/s2 (meters per second, per second.) The accelerometer is explored in greater detail later in this chapter.

· Sensor.TYPE_GRAVITY—A three-axis gravity sensor that returns the current direction and magnitude of gravity along three axes in m/s2. The gravity sensor typically is implemented as a virtual sensor by applying a low-pass filter to the accelerometer sensor results.

· Sensor.TYPE_LINEAR_ACCELERATION—A three-axis linear acceleration Sensor that returns the acceleration, not including gravity, along three axes in m/s2. Like the gravity sensor, the linear acceleration typically is implemented as a virtual sensor using the accelerometer output. In this case, to obtain the linear acceleration, a high-pass filter is applied to the accelerometer output.

· Sensor.TYPE_GYROSCOPE—A three-axis gyroscope that returns the rate of device rotation along three axes in radians/second. You can integrate the rate of rotation over time to determine the current orientation of the device; however, it generally is better practice to use this in combination with other sensors (typically the accelerometers) to provide asmoothed and corrected orientation. You'll learn more about the gyroscope Sensor later in this chapter.

· Sensor.TYPE_ROTATION_VECTOR—Returns the orientation of the device as a combination of an angle around an axis. It typically is used as an input to the getRotationMatrixFromVector method from the Sensor Manager to convert the returned rotation vector into a rotation matrix. The rotation vector Sensor typically is implemented as a virtual sensor that can combine and correct the results obtained from multiple sensors, such as the accelerometers and gyroscopes, to provide a smoother rotation matrix.

· Sensor.TYPE_MAGNETIC_FIELD—A magnetometer that finds the current magnetic field in microteslas (µT) along three axes.

· Sensor.TYPE_PRESSURE—An atmospheric pressure sensor, or barometer, that returns the current atmospheric pressure in millibars (mbars) as a single value. The pressure Sensor can be used to determine altitude using the getAltitude method on the Sensor Manager to compare the atmospheric pressure in two locations. Barometers can also be used in weather forecasting by measuring changes in atmospheric pressure in the same location.

· Sensor.TYPE_RELATIVE_HUMIDITY—A relative humidity sensor that returns the current relative humidity as a percentage. This Sensor was introduced in Android 4.0 (API level 14).

· Sensor.TYPE_PROXIMITY—A proximity sensor that indicates the distance between the device and the target object in centimeters. How a target object is selected, and the distances supported, will depend on the hardware implementation of the proximity detector. Some proximity sensors can return only “near” or “far” results, in which case the latter will be represented as the Sensor's maximum range, and the former using any lower value. Typical uses for the proximity sensor are to detect when the device is being held up against the user's ear, to automatically adjust screen brightness, or to initiate a voice command.

· Sensor.TYPE_LIGHT—An ambient light sensor that returns a single value describing the ambient illumination in lux. A light sensor commonly is used to control the screen brightness dynamically.

Introducing Virtual Sensors

Android Sensors typically work independently of each other, each reporting the results obtained from a particular piece of hardware without applying any filtering or smoothing. In some cases it can be helpful to use virtual Sensors that present simplified, corrected, or composite sensor data in a way that makes them easier to use within some applications.

The gravity, linear-acceleration, and rotation-vector Sensors described previously are examples of virtual Sensors provided by the framework. They may use a combination of accelerometers, magnetic-field sensors, and gyroscopes, rather than the output of a specific piece of hardware.

In some cases the underlying hardware will also provide virtual sensors. In such cases both the framework and hardware virtual Sensors are offered, with the default sensor being the best available.

Corrected gyroscope and orientation Sensors are also available as virtual sensors that attempt to improve the quality and performance of their respective hardware sensors. This involves using filters and the output of multiple Sensors to smooth, correct, or filter the raw output.

To ensure predictability and consistency across platforms and devices, the Sensor Manager always offers you the hardware Sensors by default. It's good practice to experiment with all the available Sensors of a given type to determine the best alternative for your particular application.

Finding Sensors

In addition to including virtual sensors, any Android device potentially could include several hardware implementations of a particular sensor type.

To find every Sensor available on the host platform, use getSensorList on the Sensor Manager, passing in Sensor.TYPE_ALL:

List<Sensor> allSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);

To find a list of all the available Sensors of a particular type, use getSensorList, specifying the type of Sensor you require, as shown in the following code that returns all the available gyroscopes:

List<Sensor> gyroscopes = sensorManager.getSensorList(Sensor.TYPE_GYROSCOPE);

If there are multiple Sensor implementations for a given sensor type, you can decide which of the returned Sensors to use by querying each returned Sensor object. Each Sensor reports its name, power use, minimum delay latency, maximum range, resolution, and vendor type. By convention, any hardware Sensor implementations are returned at the top of the list, with virtual corrected implementations last.

You can find the default Sensor implementation for a given type by using the Sensor Manager's getDefaultSensor method. If no default Sensor exists for the specified type, the method returns null.

The following snippet returns the default pressure sensor:

Sensor defaultBarometer = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);

The following code snippet shows how to select a light sensor with the highest maximum range and lowest power requirement, and the corrected gyroscope, if it's available:

List<Sensor> lightSensors 
  = sensorManager.getSensorList(Sensor.TYPE_LIGHT);
List<Sensor> gyroscopes 
  = sensorManager.getSensorList(Sensor.TYPE_GYROSCOPE);
    
Sensor bestLightSensor 
  = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
Sensor correctedGyro 
  = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
      
if (bestLightSensor != null)
  for (Sensor lightSensor : lightSensors) {
    float range = lightSensor.getMaximumRange();
    float power = lightSensor.getPower();
            
    if (range >= bestLightSensor.getMaximumRange())
      if (power < bestLightSensor.getPower() || 
          range > bestLightSensor.getMaximumRange())
        bestLightSensor = lightSensor;
  }
    
if (gyroscopes != null && gyroscopes.size() > 1)
  correctedGyro = gyroscopes.get(gyroscopes.size()-1);

2.1

Where the sensor type describes a physical hardware sensor, such as a gyroscope, the unfiltered hardware Sensor will be returned as the default in preference to any virtual implementations. In many cases the smoothing, filtering, and corrections applied to the virtual Sensor will provide better results for your applications.

It's also worth noting that some Android devices may also have multiple independent hardware sensors.

The default Sensor will always provide a Sensor implementation consistent with the typical use-case and, in most cases, will be the best alternative for your application. However, it can be useful to experiment with the available Sensors or to provide users with the ability to select which sensor to use in order to utilize the most appropriate implementation for their needs.

Monitoring Sensors

To monitor a Sensor, implement a SensorEventListener, using the onSensorChanged method to monitor Sensor values, and onAccuracyChanged to react to changes in a Sensor's accuracy.

Listing 12.1 shows the skeleton code for implementing a Sensor Event Listener.

2.11

Listing 12.1: Sensor Event Listener skeleton code

final SensorEventListener mySensorEventListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    // TODO Monitor Sensor changes.
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {
    // TODO React to a change in Sensor accuracy.
  }
};

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

The SensorEvent parameter in the onSensorChanged method includes the following four properties to describe each Sensor Event:

· sensor—The Sensor object that triggered the event.

· accuracy—The accuracy of the Sensor when the event occurred (low, medium, high, or unreliable, as described in the next list).

· values—A float array that contains the new value(s) observed. The next section explains the values returned for each sensor type.

· timestamp—The time (in nanoseconds) at which the Sensor Event occurred.

You can monitor changes in the accuracy of a Sensor separately, using the onAccuracyChanged method.

In both handlers the accuracy value represents the Sensor's accuracy, using one of the following constants:

· SensorManager.SENSOR_STATUS_ACCURACY_LOW—Indicates that the Sensor is reporting with low accuracy and needs to be calibrated

· SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM—Indicates that the Sensor data is of average accuracy and that calibration might improve the accuracy of the reported results

· SensorManager.SENSOR_STATUS_ACCURACY_HIGH—Indicates that the Sensor is reporting with the highest possible accuracy

· SensorManager.SENSOR_STATUS_UNRELIABLE—Indicates that the Sensor data is unreliable, meaning that either calibration is required or readings are not currently possible

To listen for Sensor Events, register your Sensor Event Listener with the Sensor Manager. Specify the Sensor to observe, and the rate at which you want to receive updates.

2.1

Remember that not all Sensors will be available on every device, so be sure to check for the availability of any Sensors you use, and make sure your applications fail gracefully if they are missing. Where a Sensor is required for your application to function, you can specify it as a required feature in the application's manifest, as described in Chapter 3, “Creating Applications and Activities.”

Listing 12.2 registers a Sensor Event Listener for the default proximity Sensor at the default update rate.

2.11

Listing 12.2: Registering a Sensor Event Listener

Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
sensorManager.registerListener(mySensorEventListener,
                               sensor,
                               SensorManager.SENSOR_DELAY_NORMAL);

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

The Sensor Manager includes the following static constants (shown in descending order of responsiveness) to let you specify a suitable update rate:

· SENSOR_DELAY_FASTEST—Specifies the fastest possible update rate

· SENSOR_DELAY_GAME—Specifies an update rate suitable for use in controlling games

· SENSOR_DELAY_NORMAL—Specifies the default update rate

· SENSOR_DELAY_UI—Specifies a rate suitable for updating UI features

The rate you select is not binding; the Sensor Manager may return results faster or slower than you specify, though it will tend to be faster. To minimize the associated resource cost of using the Sensor in your application, it is best practice to select the slowest acceptable rate.

It's also important to unregister your Sensor Event Listeners when your application no longer needs to receive updates:

sensorManager.unregisterListener(mySensorEventListener);

It's good practice to register and unregister your Sensor Event Listener in the onResume and onPause methods of your Activities to ensure they're being used only when the Activity is active.

Interpreting Sensor Values

The length and composition of the values returned in the onSensorChanged handler vary, depending on the Sensor being monitored. The details are summarized in Table 12.1. Further details on the use of the accelerometer, orientation, magnetic field, gyroscopic, and environmental Sensors can be found in the following sections.

2.1

The Android documentation describes the values returned by each sensor type with some additional commentary at http://developer.android.com/reference/android/hardware/SensorEvent.html.

Table 12.1 Sensor Return Values

NumberTable NumberTable NumberTable

Monitoring a Device's Movement and Orientation

Accelerometers, compasses, and (more recently) gyroscopes offer the ability to provide functionality based on device direction, orientation, and movement. You can use these Sensors to offer new and innovative input mechanisms, in addition to (or as an alternative to) traditional touch screen, trackball, and keyboard input.

The availability of specific Sensors depends on the hardware platform on which your application runs. A 70” flat screen weighs more than 150 pounds, making it difficult to lift and awkward to maneuver. As a result Android-powered TVs are unlikely to include orientation or movement sensors—so it's good practice to offer users alternatives in case their devices don't support such Sensors.

Where they are available, movement and orientation sensors can be used by your application to:

· Determine the device orientation

· React to changes in orientation

· React to movement or acceleration

· Understand which direction the user is facing

· Monitor gestures based on movement, rotation, or acceleration

This opens some intriguing possibilities for your applications. By monitoring orientation, direction, and movement, you can:

· Use the compass and accelerometers to determine your heading and orientation. Use these with a map, camera, and location-based service to create augmented-reality UIs that overlay location-based data over a real-time camera feed.

· Create UIs that adjust dynamically as the orientation of the device changes. In the most simple case, Android alters the screen orientation when the device is rotated from portrait to landscape or vice versa, but applications such as the native Gallery use orientation changes to provide a 3D effect on stacks of photos.

· Monitor for rapid acceleration to detect if a device has been dropped or thrown.

· Measure movement or vibration. For example, you could create an application that lets a user lock his or her device; if any movement is detected while it's locked, it could send an alert SMS that includes the current location.

· Create UI controls that use physical gestures and movement as input.

Determining the Natural Orientation of a Device

Before calculating the device's orientation, you must first understand its “at rest” (natural) orientation. The natural orientation of a device is the orientation in which the orientation is 0 on all three axes. The natural orientation can be either portrait or landscape, but it typically is identifiable by the placement of branding and hardware buttons.

For a typical smartphone, the natural orientation is with the device lying on its back on a desk, with the top of the device pointing due north.

More creatively, you can imagine yourself perched on top of a jet fuselage during level flight. An Android device has been strapped to the fuselage in front of you. In its natural orientation the screen is pointing up into space, the top of the device pointing towards the nose of the plane, and the plane is heading due north, as shown in Figure 12.1.

Figure 12.1

12.1

2.1

Before you head out to an airfield, note that this example is contrived to provide a useful metaphor for understanding the standard reference frame. The electronic compass and accelerometers included in most Android devices make them unsuitable for determining the heading, pitch, and roll of an aircraft in flight.

Android can reorient the display for use in any orientation; however, the Sensor axes described in Table 12.1 do not change as the device rotates. As a result, the display orientation and device orientation can be different.

Sensor values are always returned relative to the natural orientation of the device, whereas your application is likely to want the current orientation relative to the display orientation. As a result, if your application uses device orientation or linear acceleration as an input, it may be necessary to adjust your Sensor inputs based on the display orientation relative to the natural orientation. This is particularly important because the natural orientation of most early Android phones was portrait; however, with the range of Android devices having expanded to also include tablets and televisions, many Android devices (including smartphones) are naturally oriented when the display is in landscape.

To ensure that you are interpreting the orientation correctly, you need to determine the current display orientation relative to the natural orientation, rather than relying on the current display mode being either portrait or landscape.

You can find the current screen rotation using the getRotation method on the default Display object, as shown in Listing 12.3.

2.11

Listing 12.3: Finding the screen orientation relative

String windowSrvc = Context.WINDOW_SERVICE;
WindowManager wm = ((WindowManager) getSystemService(windowSrvc));
Display display = wm.getDefaultDisplay();
int rotation = display.getRotation();
switch (rotation) {
  case (Surface.ROTATION_0) : break; // Natural
  case (Surface.ROTATION_90) : break; // On its left side
  case (Surface.ROTATION_180) : break; // Upside down
  case (Surface.ROTATION_270) : break; // On its right side
  default: break;
}

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

Introducing Accelerometers

Acceleration is defined as the rate of change of velocity; that means accelerometers measure how quickly the speed of the device is changing in a given direction. Using an accelerometer you can detect movement and, more usefully, the rate of change of the speed of that movement (also known aslinear acceleration).

2.1

Accelerometers are also known as gravity sensors because they measure acceleration caused both by movement and by gravity. As a result, an accelerometer detecting acceleration on an axis perpendicular to the earth's surface will read –9.8m/s2 when it's at rest. (This value is available as the SensorManager.STANDARD_GRAVITY constant.)

Generally, you'll be interested in acceleration changes relative to a rest state, or rapid movement (signified by rapid changes in acceleration), such as gestures used for user input. In the former case you'll often need to calibrate the device to calculate the initial acceleration to take those effects into account for future results.

2.1

It's important to note that accelerometers do not measure velocity, so you can't measure speed directly based on a single accelerometer reading. Instead, you need to integrate the acceleration over time to find the velocity. You can then integrate the velocity over time to determine the distance traveled.

Because accelerometers can also measure gravity, you can use them in combination with the magnetic field sensors to calculate the device orientation. You will learn more about how to find the orientation of the device later in this section.

Detecting Acceleration Changes

Acceleration can be measured along three directional axes:

· Left-right (lateral)

· Forward-backward (longitudinal)

· Up-down (vertical)

The Sensor Manager reports accelerometer sensor changes along all three axes.

The sensor values passed in via the values property of the Sensor Event Listener's Sensor Event parameter represent lateral, longitudinal, and vertical acceleration, in that order.

Figure 12.2 illustrates the mapping of the three directional acceleration axes in relation to the device at rest in its natural orientation. Note that for the remainder of this section, I will refer to the movement of the device in relation to its natural orientation, which may be either landscape or portrait.

· x-axis (lateral)—Sideways (left or right) acceleration, for which positive values represent movement toward the right, and negative values indicate movement to the left.

· y-axis (longitudinal)—Forward or backward acceleration, for which forward acceleration, such as the device being pushed in the direction of the top of the device, is represented by a positive value and acceleration backwards represented by negative values.

· z-axis (vertical)—Upward or downward acceleration, for which positive represents upward movement, such as the device being lifted. While at rest at the device's natural orientation, the vertical accelerometer will register –9.8m/s2 as a result of gravity.

As described earlier, you can monitor changes in acceleration using a Sensor Event Listener. Register an implementation of SensorEventListener with the Sensor Manager, using a Sensor object of type Sensor.TYPE_ACCELEROMETER to request accelerometer updates. Listing 12.4 registers the default accelerometer using the default update rate.

2.11

Listing 12.4: Listerning to changes to the default accelerometer

SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
int sensorType = Sensor.TYPE_ACCELEROMETER;
sm.registerListener(mySensorEventListener,
                    sm.getDefaultSensor(sensorType),
                    SensorManager.SENSOR_DELAY_NORMAL);

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

Figure 12.2

12.2

Your Sensor Listener should implement the onSensorChanged method, which will be fired when acceleration in any direction is measured.

The onSensorChanged method receives a SensorEvent that includes a float array containing the acceleration measured along all three axes. When a device is held in its natural orientation, the first element represents lateral acceleration; the second element represents longitudinal acceleration; and the final element represents vertical acceleration, as shown in the following extension to Listing 12.4:

final SensorEventListener mySensorEventListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
      float xAxis_lateralA = sensorEvent.values[0];
      float yAxis_longitudinalA = sensorEvent.values[1];
      float zAxis_verticalA = sensorEvent.values[2];
      // TODO apply the acceleration changes to your application.
    }
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

Creating a Gravitational Force Meter

You can create a simple tool to measure gravitational force (g-force) by summing the acceleration in all three directions. In the following example you'll create a simple device to measure g-force using the accelerometers to determine the current force being exerted on the device.

The acceleration force exerted on the device at rest is 9.8 m/s2 toward the center of the Earth. In this example you'll negate the force of gravity by accounting for it using the SensorManager.STANDARD_GRAVITY constant. If you plan to use this application on another planet, you can use an alternative gravity constant, as appropriate.

1. Start by creating a new GForceMeter project that includes a ForceMeter Activity. Modify the main.xml layout resource to display two centered lines of large, bold text that will be used to display the current g-force and maximum observed g-force:

<?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">
  <TextView android:id="@+id/acceleration"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="32sp"
    android:text="Current Acceleration"
    android:editable="false"
    android:singleLine="true"
    android:layout_margin="10dp"/>
  /> 
  <TextView android:id="@+id/maxAcceleration"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="40sp"
    android:text="Maximum Acceleration"
    android:editable="false"
    android:singleLine="true"
    android:layout_margin="10dp"/>
  />
</LinearLayout>

2. Within the ForceMeter Activity, create instance variables to store references to both TextView instances and the SensorManager. Also create variables to record the current and maximum detected acceleration values:

private SensorManager sensorManager;
private TextView accelerationTextView;
private TextView maxAccelerationTextView;
private float currentAcceleration = 0;
private float maxAcceleration = 0;

3. Add a calibration constant that represents the acceleration due to gravity:

private final double calibration = SensorManager.STANDARD_GRAVITY;

4. Create a new SensorEventListener implementation that sums the acceleration detected along each axis and negates the acceleration due to gravity. It should update the current (and possibly maximum) acceleration whenever a change in acceleration is detected:

private final SensorEventListener sensorEventListener = new SensorEventListener() {
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) { }
 
  public void onSensorChanged(SensorEvent event) {
    double x = event.values[0];
    double y = event.values[1];
    double z = event.values[2];
 
    double a = Math.round(Math.sqrt(Math.pow(x, 2) +
                                    Math.pow(y, 2) +
                                    Math.pow(z, 2)));
    currentAcceleration = Math.abs((float)(a-calibration));
 
    if (currentAcceleration > maxAcceleration)
      maxAcceleration = currentAcceleration;
  }
};

5. Update the onCreate method to get a reference to the two Text Views and the Sensor Manager:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  accelerationTextView = (TextView)findViewById(R.id.acceleration);
  maxAccelerationTextView = (TextView)findViewById(R.id.maxAcceleration);
  sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
}

6. Override the onResume handler to register your new Listener for accelerometer updates using the SensorManager:

@Override
protected void onResume() {
  super.onResume();
  Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  sensorManager.registerListener(sensorEventListener,
                                 accelerometer,
                                 SensorManager.SENSOR_DELAY_FASTEST);
}

7. Also override the corresponding onPause method to unregister the sensor listener when the Activity is no longer active:

@Override
protected void onPause() {
  sensorManager.unregisterListener(sensorEventListener);
  super.onPause();
}

8. The accelerometers can update hundreds of times a second, so updating the Text Views for every change in acceleration would quickly flood the UI event queue. Instead, create a new updateGUI method that synchronizes with the GUI thread and updates the Text Views:

private void updateGUI() {
  runOnUiThread(new Runnable() {
    public void run() {
      String currentG = currentAcceleration/SensorManager.STANDARD_GRAVITY
                        + "Gs";
      accelerationTextView.setText(currentG);
      accelerationTextView.invalidate();
      String maxG = maxAcceleration/SensorManager.STANDARD_GRAVITY + "Gs";
      maxAccelerationTextView.setText(maxG);
      maxAccelerationTextView.invalidate();
    }
  });
};

This will be executed regularly using a Timer introduced in the next step.

9. Update the onCreate method to create a timer that triggers the UI update method defined in step 8 every 100 milliseconds:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  accelerationTextView = (TextView)findViewById(R.id.acceleration);
  maxAccelerationTextView = (TextView)findViewById(R.id.maxAcceleration);
  sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
 
  Sensor accelerometer =
    sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  sensorManager.registerListener(sensorEventListener,
                                 accelerometer,
                                 SensorManager.SENSOR_DELAY_FASTEST);
 
  Timer updateTimer = new Timer("gForceUpdate");
  updateTimer.scheduleAtFixedRate(new TimerTask() {
    public void run() {
      updateGUI();
    }
  }, 0, 100);
}

10. Finally, because this application is functional only when the host device features an accelerometer sensor, modify the manifest to include a uses-feature node specifying the requirement for accelerometer hardware:

<uses-feature android:name="android.hardware.sensor.accelerometer" />

2.1

All the code snippets in this example are part of the Chapter 12 GForceMeter project, available for download at www.wrox.com.

When finished, you'll want to test this out. Ideally, you can do this in an F16 while Maverick performs high-g maneuvers over the Atlantic. That's been known to end badly, so, failing that, you can experiment with spinning around in circles while holding your phone at arms length. Remember to grip your phone tightly.

Determining a Device's Orientation

Calculating a device's orientation typically is done using the combined output of both the magnetic field Sensors (which function as an electronic compass) and the accelerometers (which are used to determine the pitch and roll).

If you've done a bit of trigonometry, you've got the skills required to calculate the device orientation based on the accelerometer and magnetometer results along all three axes. If you enjoyed trig as much as I did, you'll be happy to learn that Android does these calculations for you.

It is best practice to derive the orientation using the accelerometers and magnetic field Sensors directly, as this enables you to modify the reference frame used for orientation calculations relative to the natural orientation and current display orientation.

For legacy reasons, Android also provides an orientation sensor type that provides the rotation along all three axes directly. This approach has been deprecated, but both techniques are described in the following sections.

Understanding the Standard Reference Frame

Using the standard reference frame, the device orientation is reported along three dimensions, as illustrated in Figure 12.3. As when using the accelerometers, the standard reference frame is described relative to the device's natural orientation, as described earlier inthis chapter.

Figure 12.3

12.3

Continuing the airplane analogy used early, imagining yourself perched on top of a jet fuselage during level flight, the z-axis comes out of the screen towards space; the y-axis comes out of the top of the device towards the nose of the plane; and the x-axis heads out towards the starboard wing. Relative to that, pitch, roll, and azimuth can be described as follows:

· Pitch—The angle of the device around the x-axis. During level flight, the pitch will be 0; as the nose angles upwards, the pitch increases. It will hit 90 when the jet is pointed straight up. Conversely, as you angle the nose downwards past level, the pitch will decrease until it reaches –90 as you hurtle towards imminent death. If the plane flips onto it's back the pitch will +/-180.

· Roll—The device's sideways rotation between –90 and 90 degrees around the y-axis. During level flight the roll is zero. As you execute a barrel roll towards the starboard side, the roll will increase, reaching 90 when the wings are perpendicular to the ground. As you continue, you will reach 180 when the plane is upside down. Rolling from level towards port will decrease the roll in the same way.

· Azimuth—Also heading or yaw, the azimuth is the direction the device is facing around the z-axis, where 0/360 degrees is magnetic north, 90 east, 180 south, and 270 west. Changes in the plane's heading will be reflected in changes in the azimuth value.

Calculating Orientation Using the Accelerometer and Magnetic Field Sensors

The best way to determine the current device orientation is to calculate it using the accelerometer and magnetometer results directly. In addition to providing more accurate results, this technique lets you change the orientation reference frame to remap the x-, y-, and z-axes to suit the device orientation you expect during use.

Because you're using both the accelerometer and magnetometer, you need to create and register a Sensor Event Listener to monitor each of them. Within the onSensorChanged methods for each Sensor Event Listener, record the values array property received in two separate field variables, as shown in Listing 12.5.

2.11

Listing 12.5: Monitoring the accelerometer and magnetometer

private float[] accelerometerValues;
private float[] magneticFieldValues;
 
final SensorEventListener myAccelerometerListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
      accelerometerValues = sensorEvent.values;
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
 
final SensorEventListener myMagneticFieldListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
      magneticFieldValues = sensorEvent.values;
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

Register both Sensors with the Sensor Manager, as shown in the following code extension to Listing 12.5; this snippet uses the default hardware and UI update rate for both Sensors:

SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
Sensor aSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor mfSensor = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
 
sm.registerListener(myAccelerometerListener,
                    aSensor,
                    SensorManager.SENSOR_DELAY_UI);
 
sm.registerListener(myMagneticFieldListener,
                    mfSensor,
                    SensorManager.SENSOR_DELAY_UI);

To calculate the current orientation from these Sensor values, use the getRotationMatrix and getOrientation methods from the Sensor Manager, as shown in Listing 12.6.

Listing 12.6: Finding the current orientation using the accelerometer and magnetometer

float[] values = new float[3];
float[] R = new float[9];
SensorManager.getRotationMatrix(R, null,
                                accelerometerValues,
                                magneticFieldValues);
SensorManager.getOrientation(R, values);
 
// Convert from radians to degrees if preferred.
values[0] = (float) Math.toDegrees(values[0]); // Azimuth
values[1] = (float) Math.toDegrees(values[1]); // Pitch
values[2] = (float) Math.toDegrees(values[2]); // Roll

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

Note that getOrientation returns its results in radians, not degrees. The order of the returned values is also different from the axes used by the accelerometer and magnetometer Sensors. Each result is in radians, with positive values representing anticlockwise rotation around the axis:

· values[0]—The azimuth, or rotation around the z-axis, is zero when the device is heading magnetic north.

· values[1]—The pitch, or rotation around the x-axis.

· values[2]—The roll, or rotation around the y-axis.

Remapping the Orientation Reference Frame

To measure the device orientation using a reference frame other than the natural orientation, use the remapCoordinateSystem method from the Sensor Manager. This typically is done to simplify the calculations required to create applications that can be used on devices whose natural orientation is portrait, as well as those that are landscape.

The remapCoordinateSystem method accepts four parameters:

· The initial rotation matrix, found using getRotationMatrix, as described earlier

· A variable used to store the output (transformed) rotation matrix

· The remapped x-axis

· The remapped y-axis

The Sensor Manager provides a set of constants that let you specify the remapped x- and y-axes relative to the reference frame: AXIS_X, AXIS_Y, AXIS_Z, AXIS_MINUS_X, AXIS_MINUS_Y, and AXIS_MINUS_Z.

Listing 12.7 shows how to remap the reference frame so that the current display orientation (either portrait or landscape) is used as the reference frame for calculating the current device orientation. This is useful for games or applications that are locked to either landscape or portrait mode, as the device will report either 0 or 90 degrees based on the natural orientation of the device. By modifying the reference frame, you can ensure that the orientation values you use already take into account the orientation of the display relative to the natural orientation.

2.11

Listing 12.7: Remapping the orientation reference frame based on the natural orientation of the device

// Determine the current orientation relative to the natural orientation
String windoSrvc = Context.WINDOW_SERVICE;
WindowManager wm = ((WindowManager) getSystemService(windoSrvc));
Display display = wm.getDefaultDisplay();
int rotation = display.getRotation();
 
int x_axis = SensorManager.AXIS_X; 
int y_axis = SensorManager.AXIS_Y;
 
switch (rotation) {
  case (Surface.ROTATION_0): break;
  case (Surface.ROTATION_90):  
    x_axis = SensorManager.AXIS_Y; 
    y_axis = SensorManager.AXIS_MINUS_X; 
    break;
  case (Surface.ROTATION_180): 
    y_axis = SensorManager.AXIS_MINUS_Y; 
    break;
  case (Surface.ROTATION_270): 
    x_axis = SensorManager.AXIS_MINUS_Y; 
    y_axis = SensorManager.AXIS_X; 
    break;
  default: break;
}
 
SensorManager.remapCoordinateSystem(inR, x_axis, y_axis, outR);    
 
// Obtain the new, remapped, orientation values.
SensorManager.getOrientation(outR, values);

code snippet PA4AD_Ch12_MyActivity.java

Determining Orientation Using the Deprecated Orientation Sensor

The Android framework also offers a virtual orientation Sensor.

2.1

The virtual orientation Sensor is available for legacy reasons, having been deprecated in favor of the technique described in the previous section. It was deprecated because it does not allow you to alter the reference frame used when calculating the current orientation.

To use the legacy orientation sensor, create and register a Sensor Event Listener, specifying the default orientation Sensor, as shown in Listing 12.8.

2.11

Listing 12.8: Determining orientation using the deprecated orientation Sensor

SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
int sensorType = Sensor.TYPE_ORIENTATION;
sm.registerListener(myOrientationListener,
                    sm.getDefaultSensor(sensorType),
                    SensorManager.SENSOR_DELAY_NORMAL);

code snippet PA4AD_Ch12_MyActivity.java

When the device orientation changes, the onSensorChanged method in your SensorEventListener implementation is fired. The SensorEvent parameter includes a values float array that provides the device's orientation along three axes. The following extension to Listing 12.8 shows how to construct your Sensor Event Listener:

final SensorEventListener myOrientationListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) {
      float headingAngle = sensorEvent.values[0];
      float pitchAngle =  sensorEvent.values[1];
      float rollAngle = sensorEvent.values[2];
      // TODO Apply the orientation changes to your application.
    }
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

The first element of the values array is the azimuth (heading), the second pitch, and the third roll.

Creating a Compass and Artificial Horizon

In Chapter 11, “Advanced User Experience,” you improved the CompassView to display the device pitch, roll, and heading. In this example, you'll finally connect your Compass View to the hardware sensors to display the device orientation.

1. Open the Compass project you last changed in Chapter 11 and open the CompassActivity. Use the Sensor Manager to listen for orientation changes using the magnetic field and accelerometer Sensors. Start by adding local field variables to store the last magnetic field and accelerometer values, as well as variables to store the CompassView, SensorManager, and current screen rotation values:

private float[] aValues = new float[3];
private float[] mValues = new float[3];
private CompassView compassView;
private SensorManager sensorManager;
private int rotation;

2. Create a new updateOrientation method that uses new heading, pitch, and roll values to update the CompassView:

private void updateOrientation(float[] values) {
  if (compassView!= null) {
    compassView.setBearing(values[0]);
    compassView.setPitch(values[1]);
    compassView.setRoll(-values[2]);
    compassView.invalidate();
  }
}

3. Update the onCreate method to get references to the CompassView and SensorManager, to determine the current screen orientation relative to the natural device orientation, and to initialize the heading, pitch, and roll:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  compassView = (CompassView)findViewById(R.id.compassView);
  sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
 
  String windoSrvc = Context.WINDOW_SERVICE;
  WindowManager wm = ((WindowManager) getSystemService(windoSrvc));
  Display display = wm.getDefaultDisplay();
  rotation = display.getRotation();
 
  updateOrientation(new float[] {0, 0, 0});
}

4. Create a new calculateOrientation method to evaluate the device orientation using the last recorded accelerometer and magnetic field values. Remember to account for the natural orientation of the device by remapping the reference frame, if necessary.

private float[] calculateOrientation() {
  float[] values = new float[3];
  float[] inR = new float[9];
  float[] outR = new float[9];
 
  // Determine the rotation matrix
  SensorManager.getRotationMatrix(inR, null, aValues, mValues);
 
  // Remap the coordinates based on the natural device orientation.
  int x_axis = SensorManager.AXIS_X; 
  int y_axis = SensorManager.AXIS_Y;
 
  switch (rotation) {
    case (Surface.ROTATION_90):  
      x_axis = SensorManager.AXIS_Y; 
      y_axis = SensorManager.AXIS_MINUS_X; 
      break;
    case (Surface.ROTATION_180): 
      y_axis = SensorManager.AXIS_MINUS_Y; 
      break;
    case (Surface.ROTATION_270): 
      x_axis = SensorManager.AXIS_MINUS_Y; 
      y_axis = SensorManager.AXIS_X; 
      break;
    default: break;
  }
  SensorManager.remapCoordinateSystem(inR, x_axis, y_axis, outR);    
  
  // Obtain the current, corrected orientation.
  SensorManager.getOrientation(outR, values);
 
  // Convert from Radians to Degrees.
  values[0] = (float) Math.toDegrees(values[0]);
  values[1] = (float) Math.toDegrees(values[1]);
  values[2] = (float) Math.toDegrees(values[2]);
 
  return values;
}

5. Implement a SensorEventListener as a field variable. Within onSensorChanged it should check for the calling Sensor's type and update the last accelerometer or magnetic field values, as appropriate, before making a call to updateOrientation using the calculateOrientation method.

private final SensorEventListener sensorEventListener = new SensorEventListener() {
 
  public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
      aValues = event.values;
    if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
      mValues = event.values;
 
    updateOrientation(calculateOrientation());
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

6. Override onResume and onPause to register and unregister the SensorEventListener when the Activity becomes visible and hidden, respectively:

@Override
protected void onResume() {
  super.onResume();
 
  Sensor accelerometer 
    = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  Sensor magField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
 
  sensorManager.registerListener(sensorEventListener,
                                 accelerometer,
                                 SensorManager.SENSOR_DELAY_FASTEST);
  sensorManager.registerListener(sensorEventListener,
                                 magField,
                                 SensorManager.SENSOR_DELAY_FASTEST);
}
 
@Override
protected void onPause() {
  sensorManager.unregisterListener(sensorEventListener);
  super.onPause();
}

If you run the application now, you should see the Compass View “centered” at 0, 0, 0 when the device is lying flat on a table with the top of the device pointing North. Moving the device should result in the Compass View dynamically updating as the orientation of the device changes.

You will also find that as you rotate the device through 90 degrees, the screen will rotate and the Compass View will reorient accordingly. You can extend this project by disabling automatic screen rotation.

2.1

All code snippets in this example are part of the Chapter 12 Artificial Horizon project, available for download at www.wrox.com.

Introducing the Gyroscope Sensor

Android devices increasingly are featuring a gyroscope sensor in addition to the traditional accelerometer and magnetometer sensors. The gyroscope sensor is used to measure angular speed around a given axis in radians per second, using the same coordinate system as described for the acceleration sensor.

Android gyroscopes return the rate of rotation around three axes, where their sensitivity and high frequency update rates provide extremely smooth and accurate updates. This makes them particularly good candidates for applications that use changes in orientation (as opposed to absolute orientation) as an input mechanism.

Because gyroscopes measure speed rather than direction, their results must be integrated over time in order to determine the current orientation, as shown in Listing 12.9. The calculated result will represent a change in orientation around a given axis, so you will need to either calibrate or use additional Sensors in order to determine the initial orientation.

2.11

Listing 12.9: Calculating an orientation change using the gyroscope Sensor

final float nanosecondsPerSecond = 1.0f / 100000000.0f;
private long lastTime = 0;
final float[] angle = new float[3];
  
SensorEventListener myGyroListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (lastTime != 0) {
      final float dT = (sensorEvent.timestamp - lastTime) *
                       nanosecondsPerSecond;
      angle[0] += sensorEvent.values[0] * dT;
      angle[1] += sensorEvent.values[1] * dT;
      angle[2] += sensorEvent.values[2] * dT;
    }
    lastTime = sensorEvent.timestamp;
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
    
SensorManager sm 
  = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
int sensorType = Sensor.TYPE_GYROSCOPE;
sm.registerListener(myGyroListener,
                    sm.getDefaultSensor(sensorType),
                    SensorManager.SENSOR_DELAY_NORMAL);

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

It's worth noting that orientation values derived solely from a gyroscope can become increasingly inaccurate due to calibration errors and noise. To account for this effect, gyroscopes are often used in combination with other sensors—particularly accelerometers—to provide smoother and more accurate orientation results. A virtual gyroscope was introduced in Android 4.0 (API level 14) that attempts to reduce this drift effect.

Introducing the Environmental Sensors

One of the most exciting areas of innovation in mobile hardware is the inclusion of an increasingly rich array of sensors. In addition to the orientation and movement sensors described earlier in this chapter, environmental Sensors are now becoming available in many Android devices.

Like orientation Sensors, the availability of specific environmental Sensors depends on the host hardware. Where they are available, environmental Sensors can be used by your application to:

· Improve location detection by determining the current altitude

· Track movements based on changes in altitude

· Alter the screen brightness or functionality based on ambient light

· Make weather observations and forecasts

· Determine on which planetary body the device is currently located

Using the Barometer Sensor

A barometer is used to measure atmospheric pressure. The inclusion of this sensor in some Android devices makes it possible for a user to determine his or her current altitude and, potentially, to forecast weather changes.

To monitor changes in atmospheric pressure, register an implementation of SensorEventListener with the Sensor Manager, using a Sensor object of type Sensor.TYPE_PRESSURE. The current atmospheric pressure is returned as the first (and only) value in the returned values array in hectopascals (hPa), which is an equivalent measurement to millibars (mbar).

To calculate the current altitude, you can use the static getAltitude method from the Sensor Manager, as shown in Listing 12.10, supplying it with the current pressure and the local pressure at sea level.

2.1

To ensure accurate results, you should use a local value for sea-level atmospheric pressure, although the Sensor Manager provides a value for one standard atmosphere via the PRESSURE_STANDARD_ATMOSPHERE constant as a useful approximation.

2.11

Listing 12.10: Finding the current altitude using the barometer Sensor

final SensorEventListener myPressureListener = new SensorEventListener() {
  public void onSensorChanged(SensorEvent sensorEvent) {
    if (sensorEvent.sensor.getType() == Sensor.TYPE_PRESSURE) {
      float currentPressure = sensorEvent.values[0];
          
      // Calculate altitude
      float altitude = SensorManager.getAltitude(
        SensorManager.PRESSURE_STANDARD_ATMOSPHERE, 
        currentPressure);
    }
  }
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
 
SensorManager sm 
  = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
int sensorType = Sensor.TYPE_PRESSURE;
sm.registerListener(myPressureListener,
                    sm.getDefaultSensor(sensorType),
                    SensorManager.SENSOR_DELAY_NORMAL);

code snippet PA4AD_Ch12_Sensors/src/MyActivity.java

It's important to note that getAltitude calculates altitude using the current atmospheric pressure relative to local sea-level values, not two arbitrary atmospheric pressure values. As a result, to calculate the difference in altitude between two recorded pressure values, you need to determine the altitude for each pressure and find the difference between those results, as shown in the following snippet:

float altitudeChange = 
  SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE,   
                            newPressure) -
  SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, 
                            initialPressure);

Creating a Weather Station

To fully explore the environmental Sensors available to Android devices, the following project implements a simple weather station by monitoring the barometric pressure, ambient temperature, and ambient light levels.

1. Start by creating a new WeatherStation project that includes a WeatherStation Activity. Modify the main.xml layout resource to display three centered lines of large, bold text that will be used to display the current temperature, barometric pressure, and cloud level:

<?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">
  <TextView android:id="@+id/temperature"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="28sp"
    android:text="Temperature"
    android:editable="false"
    android:singleLine="true"
    android:layout_margin="10dp"/>
  /> 
  <TextView android:id="@+id/pressure"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="28sp"
    android:text="Pressure"
    android:editable="false"
    android:singleLine="true"
    android:layout_margin="10dp"/>
  />
  <TextView android:id="@+id/light"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:textSize="28sp"
    android:text="Light"
    android:editable="false"
    android:singleLine="true"
    android:layout_margin="10dp"/>
  />
</LinearLayout>

2. Within the WeatherStation Activity, create instance variables to store references to each of the TextView instances and the SensorManager. Also create variables to record the last recorded value obtained from each sensor:

private SensorManager sensorManager;
private TextView temperatureTextView;
private TextView pressureTextView;
private TextView lightTextView;
 
private float currentTemperature = Float.NaN;
private float currentPressure = Float.NaN;
private float currentLight = Float.NaN;

3. Update the onCreate method to get a reference to the three Text Views and the Sensor Manager:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  temperatureTextView = (TextView)findViewById(R.id.temperature);
  pressureTextView = (TextView)findViewById(R.id.pressure);
  lightTextView = (TextView)findViewById(R.id.light);
  sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
}

4. Create a new SensorEventListener implementation for each of the pressure, temperature, and light sensors. Each should simply record the last recorded value:

private final SensorEventListener tempSensorEventListener
  = new SensorEventListener() {
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) { }
 
  public void onSensorChanged(SensorEvent event) {
    currentTemperature = event.values[0];
  }
};
 
private final SensorEventListener pressureSensorEventListener
  = new SensorEventListener() {
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) { }
 
  public void onSensorChanged(SensorEvent event) {
    currentPressure = event.values[0];
  }
};
 
private final SensorEventListener lightSensorEventListener
  = new SensorEventListener() {
 
  public void onAccuracyChanged(Sensor sensor, int accuracy) { }
 
  public void onSensorChanged(SensorEvent event) {
    currentLight = event.values[0];
  }
};

5. Override the onResume handler to register your new Listeners for updates using the SensorManager. Atmospheric and environmental conditions are likely to change slowly over time, so you can choose a relatively slow update rate. You should also check to confirm a default Sensor exists for each of the conditions being monitored, notifying the user where one or more Sensors are unavailable.

@Override
protected void onResume() {
  super.onResume();
  
  Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
  if (lightSensor != null)
    sensorManager.registerListener(lightSensorEventListener,
        lightSensor,
        SensorManager.SENSOR_DELAY_NORMAL);
  else
    lightTextView.setText("Light Sensor Unavailable");
 
  Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
  if (pressureSensor != null)
    sensorManager.registerListener(pressureSensorEventListener,
        pressureSensor,
        SensorManager.SENSOR_DELAY_NORMAL);
  else
    pressureTextView.setText("Barometer Unavailable");
 
  Sensor temperatureSensor =
    sensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE);
  if (temperatureSensor != null)
    sensorManager.registerListener(tempSensorEventListener,
        temperatureSensor,
        SensorManager.SENSOR_DELAY_NORMAL);
  else
    temperatureTextView.setText("Thermometer Unavailable");
}

6. Override the corresponding onPause method to unregister the Sensor Listeners when the Activity is no longer active:

@Override
protected void onPause() {
  sensorManager.unregisterListener(pressureSensorEventListener);
  sensorManager.unregisterListener(tempSensorEventListener);
  sensorManager.unregisterListener(lightSensorEventListener);
  super.onPause();
}

7. Create a new updateGUI method that synchronizes with the GUI thread and updates the Text Views. This will be executed regularly using a Timer introduced in the next step.

private void updateGUI() {
  runOnUiThread(new Runnable() {
    public void run() {
      if (!Float.isNaN(currentPressure) {
        pressureTextView.setText(currentPressure + "hPa");
        pressureTextView.invalidate();
      }
      if (!Float.isNaN(currentLight) {
        String lightStr = "Sunny";
        if (currentLight <= SensorManager.LIGHT_CLOUDY)
          lightStr = "Night";
        else if (currentLight <= SensorManager.LIGHT_OVERCAST)
          lightStr = "Cloudy";
        else if (currentLight <= SensorManager.LIGHT_SUNLIGHT)
          lightStr = "Overcast";
        lightTextView.setText(lightStr);
        lightTextView.invalidate();
      }
      if (!Float.isNaN(currentTemperature) {
        temperatureTextView.setText(currentTemperature + "C");
        temperatureTextView.invalidate();
      }
    }
  });
};

8. Update the onCreate method to create a Timer that triggers the UI update method defined in step 7 once every second:

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  temperatureTextView = (TextView)findViewById(R.id.temperature);
  pressureTextView = (TextView)findViewById(R.id.pressure);
  lightTextView = (TextView)findViewById(R.id.light);
  sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
  
  Timer updateTimer = new Timer("weatherUpdate");
  updateTimer.scheduleAtFixedRate(new TimerTask() {
    public void run() {
      updateGUI();
    }
  }, 0, 1000);
}

2.1

All code snippets in this example are part of the Chapter 12 WeatherStation project, available for download at www.wrox.com.