Chapter 14. Sensors

WHAT'S IN THIS CHAPTER?

  • Using the Sensor Manager

  • The available sensor-types

  • Monitoring sensors and interpreting sensor values

  • Using the compass, accelerometer, and orientation sensors

  • Remapping your orientation reference frame

  • Controlling device vibration

Modern mobile phones are much more than simple communications devices with a connection to the Internet. With microphones, cameras, accelerometers, compasses, temperature gauges, and brightness detectors, smartphones have become extra-sensory devices, able to augment your own perceptions.

Later chapters will explore use of the camera and microphone; in this chapter you will explore the environmental sensors potentially available on Android devices.

Sensors that detect physical and environmental properties offer an exciting innovation for enhancing the user experience of mobile applications. The incorporation of electronic compasses, gravity sensors, brightness gauges, and proximity sensors in modern devices provides an array of new possibilities for interacting with devices, such as augmented reality and physical movement-based input.

In this chapter you'll be introduced to the sensors available in Android and how to use the Sensor Manager to monitor them. You'll take a closer look at the accelerometer and orientation sensors and use them to determine changes in the device orientation and acceleration. This is particularly useful for creating motion-based user interfaces, letting you add new dimensions to your location-based applications.

You'll also learn how to control device vibration to use force feedback in your applications.

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);

Introducing Sensors

Like location-based Services, Android abstracts the sensor implementations of each device. The Sensor class is used to describe the properties of each hardware sensor, including its type, name, manufacturer, and details on its accuracy and range.

The Sensor class includes a set of constants used to describe what type of hardware sensor is being represented by a 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 those sensors.

Supported Android Sensors

The following is a list of the sensor-types currently available; note that the hardware on the host device determines which of these sensors are actually available to your application.

  • Sensor.TYPE_ACCELEROMETER A three-axis accelerometer sensor that returns the current acceleration along three axes in m/s2. The accelerometer is explored in greater detail later in this chapter.

  • Sensor.TYPE_GYROSCOPE A gyroscopic sensor that returns the current device orientation on three axes in degrees.

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

  • Sensor.TYPE_MAGNETIC_FIELD A magnetic field sensor that finds the current magnetic field in microteslas along three axes.

  • Sensor.TYPE_ORIENTATION An orientation sensor that returns the device orientation on three axes in degrees. The orientation sensor is explored in greater detail later in this chapter.

  • Sensor.TYPE_PRESSURE A pressure sensor that returns a single value, the current pressure exerted on the device in kilopascals.

  • Sensor.TYPE_PROXIMITY A proximity sensor that indicates the distance between the device and the target object in meters. How a target object is selected, and the distances supported, will depend on the hardware implementation of the proximity detector. A typical use for the proximity sensor is to detect when the device is being held up against the user's ear and to automatically adjust screen brightness or initiate a voice command.

  • Sensor.TYPE_TEMPERATURE A thermometer that returns temperature in degrees Celsius. The temperature returned may be the ambient room temperature, device battery temperature, or remote sensor temperature, depending on the hardware implementation.

Finding Sensors

An Android device can include multiple implementations of a particular sensor-type. To find the default Sensor implementation for a particular type use the Sensor Manager's getDefaultSensor method, passing in the sensor-type required from the constants described in the previous section.

The following snippet returns the default gyroscope. If no default Sensor exists for the given type, the method returns null.

Sensor defaultGyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);

Alternatively, use getSensorList to return a list of all the available Sensors of a given type, as shown in the following code, which returns all the available pressure sensor objects:

List<Sensor> pressureSensors = sensorManager.getSensorList(Sensor.TYPE_PRESSURE);

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

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

This technique lets you determine which Sensors, and sensor-types, are available on the host platform.

Using Sensors

Listing 14-1 shows the standard pattern for monitoring hardware sensor results. Later sections will take a closer look at orientation and acceleration Sensor implementations in particular.

Implement a SensorEventListener. Use the onSensorChanged method to monitor Sensor values and onAccuracyChanged to react to changes in a Sensor's accuracy.

Example 14-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.
  }
};

The SensorEvent parameter in the onSensorChanged method includes four properties used to describe a 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) detected. 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 feedback from the monitored 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 readings

  • 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 receive Sensor events, register your Sensor Event Listener with the Sensor Manager. Specify the Sensor object to observe, and the rate at which you want to receive updates. The following example registers a Sensor Event Listener for the default proximity Sensor at the normal update rate:

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

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

  • SensorManager.SENSOR_DELAY_FASTEST Specifies the fastest possible Sensor update rate

  • SensorManager.SENSOR_DELAY_GAME Selects an update rate suitable for use in controlling games

  • SensorManager.SENSOR_DELAY_NORMAL Specifies the default update rate

  • SensorManager.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 you should try to select the slowest suitable 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 event vary depending on the Sensor being monitored.

The details are summarized in Table 14-1. Further details on the use of the accelerometer, orientation, and magnetic field Sensors can be found in the following sections.

Note

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

Table 14-1. Sensor Return Values

SENSOR-TYPE

VALUE COUNT

VALUE COMPOSITION

COMMENTARY

TYPE_ACCELEROMETER

3

value[0] : Lateral

value[1] : Longitudinal

value[2] : Vertical

Acceleration along three axes in m/s2. The Sensor Manager includes a set of gravity constants of the form SensorManager.GRAVITY_*

TYPE_GYROSCOPE

3

value[0] : Azimuth

value[1] : Pitch

value[2] : Roll

Device orientation in degrees along three axes.

TYPE_ LIGHT

1

value[0] : Illumination

Measured in lux. The Sensor Manager includes a set of constants representing different standard illuminations of the form SensorManager.LIGHT_*

TYPE_MAGNETIC_FIELD

3

value[0] : Lateral

value[1] : Longitudinal

value[2] : Vertical

Ambient magnetic field measured in microteslas (µT).

TYPE_ORIENTATION

3

value[0] : Azimuth

value[1] : Roll

value[2] : Pitch

Device orientation in degrees along three axes.

TYPE_PRESSURE

1

value[0] : Pressure

Measured in kilopascals (KP).

TYPE_PROXIMITY

1

value[0] : Distance

Measured in meters.

TYPE_TEMPERATURE

1

value[0] : Temperature

Measured in degrees Celsius.

USING THE COMPASS, ACCELEROMETER, AND ORIENTATION SENSORS

Using movement and orientation within applications is possible thanks to the inclusion of orientation and accelerometer sensors in many modern devices.

In recent years these sensors have become increasingly common, having found their way into game controllers like the Nintendo Wii and mobile smartphone handsets like the Apple iPhone, Palm Pre, and many Android devices.

Accelerometers and compasses are used to provide functionality based on device direction, orientation, and movement. A recent trend is to use this functionality to provide input mechanisms other than the traditional touchscreen, trackball, and keyboard.

The availability of compass and accelerometer Sensors depends on the hardware on which your application runs. When available, they are exposed through the Sensor Manager, allowing you to do the following:

  • Determine the current device orientation

  • Monitor and track changes in orientation

  • Know which direction the user is facing

  • Monitor acceleration — changes in movement rate — in any direction: vertically, laterally, or longitudinally

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

  • Use the compass and accelerometer to determine your speed and direction. Use these with a map, camera, and location-based services to create augmented reality interfaces that overlay location-based data over the real-time camera feed.

  • Create user interfaces that adjust dynamically to suit the orientation of your device. Android already alters the native screen orientation when the device is rotated from portrait to landscape or vice versa.

  • 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 you lock your device; if any movement is detected while it's locked it could send an alert SMS that includes the current location.

  • Create user interface controls that use physical gestures and movement as input.

You should always check for the availability of any required Sensors and make sure your applications fail gracefully if they are missing.

Introducing Accelerometers

Accelerometers, as their name suggests, are used to measure acceleration. They are also sometimes referred to as gravity sensors.

Note

Accelerometers are also known as gravity sensors because of their inability to differentiate between acceleration caused by movement and gravity. As a result, an accelerometer detecting acceleration on the z-axis (up/down) will read −9.8m/s2 when it's at rest (this value is available as the SensorManager.STANDARD_GRAVITY constant).

Acceleration is defined as the rate of change of velocity, so 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.

Note

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 measure changes in acceleration over time.

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 orientation and acceleration to take those effects into account for future results.

Detecting Acceleration Changes

Acceleration can be measured along three directional axes: left-right (lateral), forward-backward (longitudinal), and up-down (vertical). The Sensor Manager reports accelerometer Sensor changes along all three axes.

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

Figure 14-1 illustrates the mapping of the three directional acceleration axes in relation to the device at rest. The Sensor Manager considers the device "at rest" when it is sitting face up on a flat surface in portrait orientation.

FIGURE 14-1

Figure 14-1. FIGURE 14-1

  • x-axis (lateral) Sideways (left or right) acceleration, for which positive values represent movement toward the right side of the device, and negative values indicate movement to the left. For example, positive x-axis acceleration would be detected on a device flat on its back, facing up, and in portrait orientation being moved along a surface to your right.

  • y-axis (longitudinal) Forward or backward acceleration, for which forward acceleration is represented by a positive value. In the same configuration as described for lateral movement, you would create positive longitudinal acceleration by moving the device in the direction of the top of the device.

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

As described earlier, you 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 14-2 registers the default accelerometer using the normal update rate.

Example 14-2. Listening 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);

Your Sensor Listener should implement the onSensorChanged method that 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. Based on a rest state of the device sitting flat on its back in portrait orientation, the first element represents lateral, the second longitudinal, and the final vertical acceleration, as shown in the following extension to Listing 14-2.

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.
    }
  }
};

Creating a G-Forceometer

You can create a simple tool to measure g-force by summing the acceleration in all three directions and comparing it to the value in free fall. 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.

Thanks to gravity the force exerted on the device at rest is 9.8m/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.

  1. Start by creating a new Forceometer project with a Forceometer 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="fill_parent"
      android:layout_height="fill_parent">
      <TextView android:id="@+id/acceleration"
        android:gravity="center"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="32sp"
        android:text="CENTER"
        android:editable="false"
        android:singleLine="true"
        android:layout_margin="10px"/>
      />
      <TextView android:id="@+id/maxAcceleration"
        android:gravity="center"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="40sp"
        android:text="CENTER"
        android:editable="false"
        android:singleLine="true"
        android:layout_margin="10px"/>
      />
    </LinearLayout>
  2. Within the Forceometer Activity, create instance variables to store references to both TextView Views and the SensorManager. Also create variables to record the last and maximum detected acceleration values:

    SensorManager sensorManager;
    TextView accelerationTextView;
    TextView maxAccelerationTextView;
    float currentAcceleration = 0;
    float maxAcceleration = 0;
  3. Create a new SensorEventListener implementation that sums the acceleration detected along each axis and negates the acceleration caused by gravity. It should update the current and maximum acceleration whenever a change in acceleration is detected:

    private final SensorEventListener sensorEventListener = new SensorEventListener() {
      double calibration = SensorManager.STANDARD_GRAVITY;
    
      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;
      }
    };
  4. Update the onCreate method to register your new Listener for accelerometer updates using the SensorManager. Take the opportunity to get a reference to the two Text Views:

    @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);
    }
  5. The accelerometers can be very sensitive, so updating the Text Views for every detected acceleration change can be very expensive. Instead, create a new updateGUI method that synchronizes with the GUI thread based on a Timer before updating 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();
        }
      });
    };
  6. Finally, update the onCreate method to start a timer that's used to update the GUI every 100ms:

    @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);
    }
    
    Creating a G-Forceometer

Once you're finished you'll want to test this out. Ideally you can do that 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 running or driving in the safety of your neighborhood.

Given that keeping constant watch on your handset while driving, cycling, or flying is also likely to end poorly, you might consider some further enhancements before you take it out for a spin.

Consider incorporating vibration or media player functionality to shake or beep with an intensity proportional to your current force, or simply log changes as they happen for later review.

Determining Your Orientation

The orientation Sensor is a combination of the magnetic field Sensors, which function as an electronic compass, and accelerometers, which 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 magnetic field values 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.

In fact, Android provides two alternatives for determining the device orientation. You can query the orientation Sensor directly or derive the orientation using the accelerometers and magnetic field Sensors. The latter option is slower, but offers the advantages of increased accuracy and the ability to modify the reference frame when determining your orientation. The following sections demonstrate both techniques.

Using the standard reference frame, the device orientation is reported along three dimensions, as illustrated in Figure 14-2. As when using the accelerometers, the device is considered at rest faceup on a flat surface.

FIGURE 14-2

Figure 14-2. FIGURE 14-2

  • x-axis (azimuth) The azimuth (also heading or yaw) is the direction the device is facing around the x-axis, where 0/360 degrees is north, 90 east, 180 south, and 270 west.

  • y-axis (pitch) Pitch represents the angle of the device around the y-axis. The tilt angle returned shows 0 when the device is flat on its back, −90 when it is standing upright (top of device pointing at the ceiling), 90 when it's upside down, and 180/-180 when it's facedown.

  • z-axis (roll) The roll represents the device's sideways tilt between −90 and 90 degrees on the z-axis. Zero is the device flat on its back, −90 is the screen facing left, and 90 is the screen facing right.

Determining Orientation Using the Orientation Sensor

The simplest way to monitor device orientation is by using a dedicated orientation Sensor. Create and register a Sensor Event Listener with the Sensor Manager, using the default orientation Sensor, as shown in Listing 14-3.

Example 14-3. Determining orientation using the orientation Sensor

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

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 first element of the values array is the azimuth (heading), the second pitch, and the third roll.

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) {}
};

Calculating Orientation Using the Accelerometer and Magnetic Field Sensors

The best approach for finding the device orientation is to calculate it from the accelerometer and magnetic field Sensor results directly.

This technique enables you to change the orientation reference frame to remap the x-, y-, and z-axes to suit the device orientation you expect during use.

This approach uses both the accelerometer and magnetic field Sensors, so you need to create and register two Sensor Event Listeners. Within the onSensorChanged methods for each Sensor Event Listener, record the values array property received in two separate field variables, as shown in Listing 14-4.

Example 14-4. Finding orientation using the accelerometer and magnetic field Sensors

float[] accelerometerValues;
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) {}
};

Register both with the Sensor Manager, as shown in the following code extending Listing 14-4; 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 you use the getRotationMatrix and getOrientation methods from the Sensor Manager, as follows. Note that getOrientation returns radians rather than degrees.

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.
values[0] = (float) Math.toDegrees(values[0]);
values[1] = (float) Math.toDegrees(values[1]);
values[2] = (float) Math.toDegrees(values[2]);

Remapping the Orientation Reference Frame

To measure device orientation using a reference frame other than the default described earlier, use the remapCoordinateSystem method from the Sensor Manager.

Earlier in this chapter the standard reference frame was described as the device being faceup on a flat surface. This method lets you remap the coordinate system used to calculate your orientation, for example by specifying the device to be at rest when mounted vertically.

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

Two final parameters are used to specify the new reference frame. The values used specify the new x- and y-axes relative to the default frame. The Sensor Manager provides a set of constants to let you specify the axis values: AXIS_X, AXIS_Y, AXIS_Z, AXIS_MINUS_X, AXIS_MINUS_Y, and AXIS_MINUS_Z.

Listing 14-5 shows how to remap the reference frame so that a device is at rest when mounted vertically — held in portrait mode with its screen facing the user — as shown in Figure 14-3.

FIGURE 14-3

Figure 14-3. FIGURE 14-3

Example 14-5. Remapping the orientation reference frame

SensorManager.getRotationMatrix(R, null, aValues, mValues);

float[] outR = new float[9];
SensorManager.remapCoordinateSystem(R,
                                    SensorManager.AXIS_X,
                                    SensorManager.AXIS_Z,
                                    outR);
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]);

Creating a Compass and Artificial Horizon

In Chapter 4 you created a simple CompassView to experiment with owner-drawn controls. In this example you'll extend the functionality of the Compass View to display the device pitch and roll, before using it to display the device orientation.

  1. Open the Compass project you created in Chapter 4. You will be making changes to the CompassView as well as the Compass Activity used to display it. To ensure that the view and controller remain as decoupled as possible, the CompassView won't be linked to the Sensors directly; instead it will be updated by the Activity. Start by adding field variables and get/set methods for pitch and roll to the CompassView.

    float pitch = 0;
    float roll = 0;
    
    public float getPitch() {
      return pitch;
    }
    public void setPitch(float pitch) {
      this.pitch = pitch;
    }
    
    public float getRoll() {
      return roll;
    }
    public void setRoll(float roll) {
      this.roll = roll;
    }
  2. Update the onDraw method to include two circles that will be used to indicate the pitch and roll values.

    @Override
    protected void onDraw(Canvas canvas) {
    
      [ ... Existing onDraw method ... ]
    • 2.1. Create a new circle that's half filled and rotates in line with the sideways tilt (roll).

      RectF rollOval = new RectF((mMeasuredWidth/3)-mMeasuredWidth/7,
                                   (mMeasuredHeight/2)-mMeasuredWidth/7,
                                   (mMeasuredWidth/3)+mMeasuredWidth/7,
                                   (mMeasuredHeight/2)+mMeasuredWidth/7
                                  );
        markerPaint.setStyle(Paint.Style.STROKE);
        canvas.drawOval(rollOval, markerPaint);
        markerPaint.setStyle(Paint.Style.FILL);
        canvas.save();
        canvas.rotate(roll, mMeasuredWidth/3, mMeasuredHeight/2);
        canvas.drawArc(rollOval, 0, 180, false, markerPaint);
      
        canvas.restore();
    • 2.2. Create a new circle that starts half filled and varies between full and empty based on the forward angle (pitch):

      RectF pitchOval = new RectF((2*mMeasuredWidth/3)-mMeasuredWidth/7,
                                    (mMeasuredHeight/2)-mMeasuredWidth/7,
                                    (2*mMeasuredWidth/3)+mMeasuredWidth/7,
                                    (mMeasuredHeight/2)+mMeasuredWidth/7
                                   );
      markerPaint.setStyle(Paint.Style.STROKE);
        canvas.drawOval(pitchOval, markerPaint);
        markerPaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(pitchOval, 0-pitch/2, 180+(pitch), false, markerPaint);
        markerPaint.setStyle(Paint.Style.STROKE);
      }
  3. That completes the changes to the CompassView. If you run the application now it should appear as shown in Figure 14-4.

    FIGURE 14-4

    Figure 14-4. FIGURE 14-4

  4. Now update the Compass Activity. 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 references to the CompassView and SensorManager.

    float[] aValues = new float[3];
    float[] mValues = new float[3];
    CompassView compassView;
    SensorManager sensorManager;
  5. 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();
      }
    }
  6. Update the onCreate method to get references to the CompassView and SensorManager, and initialize the heading, pitch, and roll.

    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
    
      compassView = (CompassView)this.findViewById(R.id.compassView);
      sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
      updateOrientation(new float[] {0, 0, 0});
    }
  7. Create a new calculateOrientation method to evaluate the device orientation using the last recorded accelerometer and magnetic field values.

    private float[] calculateOrientation() {
      float[] values = new float[3];
      float[] R = new float[9];
    
      SensorManager.getRotationMatrix(R, null, aValues, mValues);
      SensorManager.getOrientation(R, 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;
    }
  8. 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) {}
    };
  9. Now override onResume and onStop 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 onStop()
    {
      sensorManager.unregisterListener(sensorEventListener);
      super.onStop();
    }

    If you run the application now you should see the three face dials update dynamically when the orientation of the device changes.

  10. An artificial horizon is more useful if it's mounted vertically. Modify the reference frame of the artificial horizon to match this orientation by updating calculateOrientation to remap the coordinate system.

    private float[] calculateOrientation() {
        float[] values = new float[3];
        float[] R = new float[9];
        float[] outR = new float[9];
    
        SensorManager.getRotationMatrix(R, null, aValues, mValues);
        SensorManager.remapCoordinateSystem(R,
                                            SensorManager.AXIS_X,
                                            SensorManager.AXIS_Z,
                                            outR);
    
        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;
      }
    
    FIGURE 14-4

CONTROLLING DEVICE VIBRATION

In Chapter 9 you learned how to create Notifications that can use vibration to enrich event feedback. In some circumstances you may want to vibrate the device independently of Notifications. Vibrating the device is an excellent way to provide haptic user feedback, and is particularly popular as a feedback mechanism for games.

To control device vibration, your applications needs the VIBRATE permission. Add this to your application manifest using the following XML snippet:

<uses-permission android:name="android.permission.VIBRATE"/>

Device vibration is controlled through the Vibrator Service, accessible via the getSystemService method, as shown in Listing 14-6.

Example 14-6. Controlling device vibration

String vibratorService = Context.VIBRATOR_SERVICE;
Vibrator vibrator = (Vibrator)getSystemService(vibratorService);

Call vibrate to start device vibration; you can pass in either a vibration duration or a pattern of alternating vibration/pause sequences along with an optional index parameter that will repeat the pattern starting at the index specified. Both techniques are demonstrated in the following extension to Listing 14-6:

long[] pattern = {1000, 2000, 4000, 8000, 16000 };
vibrator.vibrate(pattern, 0); // Execute vibration pattern.
vibrator.vibrate(1000); // Vibrate for 1 second.

To cancel vibration call cancel; exiting your application will automatically cancel any vibration it has initiated.

SUMMARY

In this chapter you learned how to use the Sensor Manager to let your application respond to the physical environment. You were introduced to the Sensors available on the Android platform and learned how to listen for Sensor Events using the Sensor Event Listener and how to interpret those results.

Then you took a more detailed look at the accelerometer, orientation, and magnetic field detection hardware, using these Sensors to determine the device's orientation and acceleration. In the process you created a g-forceometer and an artificial horizon.

You also learned:

  • Which Sensors are available to Android applications

  • How to remap the reference frame when determining a device's orientation

  • The composition and meaning of the Sensor Event values returned by each sensor

  • How to use device vibration to provide physical feedback for application events

In the final chapter, you'll be introduced to some of the advanced Android features. You'll learn more about security, how to use AIDL to facilitate interprocess communication, and using Wake Locks. You'll be introduced to Android's TTS library and learn about Android's User Interface and graphics capabilities by exploring animations and advanced Canvas drawing techniques. Finally, you'll be introduced to the SurfaceView and touch-screen input functionality.

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

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