2

I have an Android app that's using the Rotation Vector sensor. Having read the comment here, I see that with newer API levels, the SensorEvent contains 4-5 values, instead of 3. My code uses an arraycopy for the data.

lastRotVal is a class member and is initialized as an array of size [3]. Here's the relevant parts of the code responding to the sensor events.

public void onSensorChanged(SensorEvent event) {
   System.arraycopy(event.values, 0, lastRotVal, 0, 3); //Hardcoded size
   lastRotValSet = true;
   updateDisplay(event);
}

private void updateDisplay(SensorEvent event){

   if (lastRotValSet){  
       float[] rotation = new float[9];
       float[] orientation = new float[3];

       SensorManager.getRotationMatrixFromVector(rotation, lastRotVal);
       SensorManager.getOrientation(rotation, orientation);
       double pitch = orientation[1];
       double roll = orientation[2];

       //Do stuff with roll and pitch
   }
}

I've hardcoded in only using 3 values in the arraycopy. It seems to be working on both older and the new API levels. Is this the best way to maintain that compatibility between versions, or could I be doing something better?

Edit: As stated by the accepted answer, the IllegalArgumentException that prompted this question apparently arises as a consequence of a bug in the API on Samsung devices, not the general API version. So I'll add the fact that my initial error was encountered on a Samsung Galaxy SIII.

Community
  • 1
  • 1
jranalli
  • 718
  • 1
  • 6
  • 14
  • Interesting - what variant of the Galaxy S3 did you see this error on? I didn't encounter it on U.S. Sprint variant SPH-L710 with Android 4.3. – Sean Barbeau Mar 04 '14 at 01:16

2 Answers2

3

The post that you reference actually refers to a bug in a few Samsung devices (Galaxy S4, Galaxy Note 3) - see this Android Developer list post. You actually shouldn't have to do any special handling between SDK levels for this code to work on normal devices. But, alas, fragmentation...

Chromium handles this issue by truncating the array if the size is greater than 4:

if (values.length > 4) {
    // On some Samsung devices SensorManager.getRotationMatrixFromVector
    // appears to throw an exception if rotation vector has length > 4.
    // For the purposes of this class the first 4 values of the
    // rotation vector are sufficient (see crbug.com/335298 for details).
    if (mTruncatedRotationVector == null) {
        mTruncatedRotationVector = new float[4];
    }
    System.arraycopy(values, 0, mTruncatedRotationVector, 0, 4);
        getOrientationFromRotationVector(mTruncatedRotationVector);
} else {
    getOrientationFromRotationVector(values);
}

However, I found in my app GPSTest that this solution didn't seem to work on the Galaxy S3 (see Github issue here).

So, I ended up only truncating the array on devices that throw the IllegalArgumentException. This also avoids the extra System.arraycopy() unless its absolutely necessary.

Here's the code snippet (that also supports orientation sensors on devices with API levels less than Gingerbread (i.e., prior to when the ROTATION_VECTOR sensor was introduced), and handles remapping the coordinate system for orientation changes), which uses a class member mTruncateVector that is initialized to false:

@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void onSensorChanged(SensorEvent event) {

    double orientation = Double.NaN;
    double tilt = Double.NaN;

    switch (event.sensor.getType()) {
        case Sensor.TYPE_ROTATION_VECTOR:
            // Modern rotation vector sensors
            if (!mTruncateVector) {
                try {
                    SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
                } catch (IllegalArgumentException e) {
                    // On some Samsung devices, an exception is thrown if this vector > 4 (see #39)
                    // Truncate the array, since we can deal with only the first four values
                    Log.e(TAG, "Samsung device error? Will truncate vectors - " + e);
                    mTruncateVector = true;
                    // Do the truncation here the first time the exception occurs
                    getRotationMatrixFromTruncatedVector(event.values);
                }
            } else {
                // Truncate the array to avoid the exception on some devices (see #39)
                getRotationMatrixFromTruncatedVector(event.values);
            }

            int rot = getWindowManager().getDefaultDisplay().getRotation();
            switch (rot) {
                case Surface.ROTATION_0:
                    // No orientation change, use default coordinate system
                    SensorManager.getOrientation(mRotationMatrix, mValues);
                    // Log.d(TAG, "Rotation-0");
                    break;
                case Surface.ROTATION_90:
                    // Log.d(TAG, "Rotation-90");
                    SensorManager.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_Y,
                            SensorManager.AXIS_MINUS_X, mRemappedMatrix);
                    SensorManager.getOrientation(mRemappedMatrix, mValues);
                    break;
                case Surface.ROTATION_180:
                    // Log.d(TAG, "Rotation-180");
                    SensorManager
                            .remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_MINUS_X,
                                    SensorManager.AXIS_MINUS_Y, mRemappedMatrix);
                    SensorManager.getOrientation(mRemappedMatrix, mValues);
                    break;
                case Surface.ROTATION_270:
                    // Log.d(TAG, "Rotation-270");
                    SensorManager
                            .remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_MINUS_Y,
                                    SensorManager.AXIS_X, mRemappedMatrix);
                    SensorManager.getOrientation(mRemappedMatrix, mValues);
                    break;
                default:
                    // This shouldn't happen - assume default orientation
                    SensorManager.getOrientation(mRotationMatrix, mValues);
                    // Log.d(TAG, "Rotation-Unknown");
                    break;
            }
            orientation = Math.toDegrees(mValues[0]);  // azimuth
            tilt = Math.toDegrees(mValues[1]);
            break;
        case Sensor.TYPE_ORIENTATION:
            // Legacy orientation sensors
            orientation = event.values[0];
            break;
        default:
            // A sensor we're not using, so return
            return;
    }
}

@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private void getRotationMatrixFromTruncatedVector(float[] vector) {
    System.arraycopy(vector, 0, mTruncatedRotationVector, 0, 4);
    SensorManager.getRotationMatrixFromVector(mRotationMatrix, mTruncatedRotationVector);
}

and to register the sensors in onResume():

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
        // Use the modern rotation vector sensors
        Sensor vectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        mSensorManager.registerListener(this, vectorSensor, 16000); // ~60hz
    } else {
        // Use the legacy orientation sensors
        Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
        if (sensor != null) {
            mSensorManager.registerListener(this, sensor,
                    SensorManager.SENSOR_DELAY_GAME);
        }
    }

Full implementation is here on Github.

Community
  • 1
  • 1
Sean Barbeau
  • 11,496
  • 8
  • 58
  • 111
  • 1
    I didn't realize that this was a specific bug related to the Samsung devices, so that's good extra info. It just happened that the only 4.3 device I've had to test on was a Galaxy SIII. Good to know that it's not a more general problem. – jranalli Mar 03 '14 at 13:35
1

Short summary: Presuming you don't want to make use of values[3] and values[4], your code is fine as is.

From the docs:

values[0]: x*sin(θ/2)
values[1]: y*sin(θ/2)
values[2]: z*sin(θ/2)
values[3]: cos(θ/2)
values[4]: estimated heading Accuracy (in radians) (-1 if unavailable)
values[3], originally optional, will always be present from SDK Level 18 onwards. values[4] is a new value that has been added in SDK Level 18.

If I read that right, event.values.length will only be greater 3 if you compiled with SDK 18 or older.

SensorManager.getRotationMatrixFromVector seems to assume a rotation vector of length==3. I'm not certain what that function does if the rotation vector passed in is larger than 3 elements anyway.

If you ever needed to make use of the event.values[3] and event.values[4], you could detect if the device supports these extended values simply by checking the event.values.length. You could also check to see if Build.VERSION.SDK_INT is >= 18 at runtime as well. But if you don't need it, stick with your hardcoded assumption of 3.

selbie
  • 100,020
  • 15
  • 103
  • 173
  • I looked at the source code for getRotationMatrixFromVector, and it only pulls the first 3 values, and so no difference. Who knows what could happen to that in the future though. See the accepted answer as to why the event.values.length didn't work in this case. It's just some weird bug with the Samsung devices. – jranalli Mar 03 '14 at 13:37