// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.components.runtime; import com.google.appinventor.components.annotations.DesignerComponent; import com.google.appinventor.components.annotations.DesignerProperty; import com.google.appinventor.components.annotations.PropertyCategory; import com.google.appinventor.components.annotations.SimpleEvent; import com.google.appinventor.components.annotations.SimpleObject; import com.google.appinventor.components.annotations.SimpleProperty; import com.google.appinventor.components.common.ComponentCategory; import com.google.appinventor.components.common.PropertyTypeConstants; import com.google.appinventor.components.common.YaVersion; import com.google.appinventor.components.runtime.util.FroyoUtil; import com.google.appinventor.components.runtime.util.OrientationSensorUtil; import com.google.appinventor.components.runtime.util.SdkLevel; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.util.Log; import android.view.Display; import android.view.Surface; import android.view.WindowManager; /** * Sensor that can measure absolute orientation in 3 dimensions. * */ @DesignerComponent(version = YaVersion.ORIENTATIONSENSOR_COMPONENT_VERSION, description = "<p>Non-visible component providing information about the " + "device's physical orientation in three dimensions: <ul> " + "<li> <strong>Roll</strong>: 0 degrees when the device is level, increases to " + " 90 degrees as the device is tilted up on its left side, and " + " decreases to -90 degrees when the device is tilted up on its right side. " + " </li> " + "<li> <strong>Pitch</strong>: 0 degrees when the device is level, up to " + " 90 degrees as the device is tilted so its top is pointing down, " + " up to 180 degrees as it gets turned over. Similarly, as the device " + " is tilted so its bottom points down, pitch decreases to -90 " + " degrees, then further decreases to -180 degrees as it gets turned all the way " + " over.</li> " + "<li> <strong>Azimuth</strong>: 0 degrees when the top of the device is " + " pointing north, 90 degrees when it is pointing east, 180 degrees " + " when it is pointing south, 270 degrees when it is pointing west, " + " etc.</li></ul>" + " These measurements assume that the device itself is not moving.</p>", category = ComponentCategory.SENSORS, nonVisible = true, iconName = "images/orientationsensor.png") @SimpleObject public class OrientationSensor extends AndroidNonvisibleComponent implements SensorEventListener, Deleteable, OnPauseListener, OnResumeListener { // Constants private static final String LOG_TAG = "OrientationSensor"; // offsets in array returned by SensorManager.getOrientation() private static final int AZIMUTH = 0; private static final int PITCH = 1; private static final int ROLL = 2; private static final int DIMENSIONS = 3; // Warning: specific to our universe // Properties private boolean enabled; private float azimuth; // degrees private float pitch; // degrees private float roll; // degrees private int accuracy; // Sensor information private final SensorManager sensorManager; private final Sensor accelerometerSensor; private final Sensor magneticFieldSensor; private boolean listening; // Pre-allocated arrays to hold sensor data so that we don't cause so many garbage collections // while processing sensor events. All are used only in onSensorChanged. private final float[] accels = new float[DIMENSIONS]; // acceleration vector private final float[] mags = new float[DIMENSIONS]; // magnetic field vector // Flags to tell whether the above arrays are filled. They are set in onSensorChanged and cleared // in stopListening. private boolean accelsFilled; private boolean magsFilled; // Pre-allocated matrixes used to compute orientation values from acceleration and magnetic // field data. private final float[] rotationMatrix = new float[DIMENSIONS * DIMENSIONS]; private final float[] inclinationMatrix = new float[DIMENSIONS * DIMENSIONS]; private final float[] values = new float[DIMENSIONS]; /** * Creates a new OrientationSensor component. * * @param container ignored (because this is a non-visible component) */ public OrientationSensor(ComponentContainer container) { super(container.$form()); // Get sensors, and start listening. sensorManager = (SensorManager) container.$context().getSystemService(Context.SENSOR_SERVICE); accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); // Begin listening in onResume() and stop listening in onPause(). form.registerForOnResume(this); form.registerForOnPause(this); // Set default property values. Enabled(true); } private void startListening() { if (!listening) { sensorManager.registerListener(this, accelerometerSensor, SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(this, magneticFieldSensor, SensorManager.SENSOR_DELAY_NORMAL); listening = true; } } private void stopListening() { if (listening) { sensorManager.unregisterListener(this); listening = false; // Throw out sensor information that will go stale. accelsFilled = false; magsFilled = false; } } // Events /** * Default OrientationChanged event handler. * * <p>This event is signalled when the device's orientation has changed. It * reports the new values of azimuth, pich, and roll, and it also sets the Azimuth, Pitch, * and roll properties.</p> * <p>Azimuth is the compass heading in degrees, pitch indicates how the device * is tilted from top to bottom, and roll indicates how much the device is tilted from * side to side.</p> */ @SimpleEvent public void OrientationChanged(float azimuth, float pitch, float roll) { EventDispatcher.dispatchEvent(this, "OrientationChanged", azimuth, pitch, roll); } // Properties /** * Available property getter method (read-only property). * * @return {@code true} indicates that an orientation sensor is available, * {@code false} that it isn't */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public boolean Available() { return sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() > 0 && sensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD).size() > 0; } /** * Enabled property getter method. * * @return {@code true} indicates that the sensor generates events, * {@code false} that it doesn't */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public boolean Enabled() { return enabled; } /** * Enabled property setter method. * * @param enabled {@code true} enables sensor event generation, * {@code false} disables it */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "True") @SimpleProperty public void Enabled(boolean enabled) { if (this.enabled != enabled) { this.enabled = enabled; if (enabled) { startListening(); } else { stopListening(); } } } /** * Pitch property getter method (read-only property). * * <p>To return meaningful values the sensor must be enabled.</p> * * @return current pitch */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public float Pitch() { return pitch; } /** * Roll property getter method (read-only property). * * <p>To return meaningful values the sensor must be enabled.</p> * * @return current roll */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public float Roll() { return roll; } /** * Azimuth property getter method (read-only property). * * <p>To return meaningful values the sensor must be enabled.</p> * * @return current azimuth */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public float Azimuth() { return azimuth; } /** * <p>Angle property getter method (read-only property). Specifically, this * provides the angle in which the orientation sensor is tilted, treating * -{@link #Roll()} as the x-coordinate and {@link #Pitch()} as the * y-coordinate. For the amount of the tilt, use {@link #Magnitude()}.</p> * * <p>To return meaningful values the sensor must be enabled.</p> * * @return the angle in degrees */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public float Angle() { return OrientationSensor.computeAngle(pitch, roll); } /** * Computes the angle the phone is tilted. This has been lifted out * of {@link #Angle()} for ease of testing. * * @param pitch an angle indicating how far the device is tilted vertically, * with a value of +90 degrees if the top is pointing straight * down, +180 degrees if the phone is entirely turned over, * -90/+270 degrees if the top is pointing straight up, etc. * @param roll an angle indicating how far the device is tilted horizontally, * with a value of +90 degrees if it is tilted entirely on its * left side, -90 degrees if it is tilted entirely on its right * side; the maximum absolute value of roll is 90 degrees, after * which it decreases back toward 0 (flat face-up or face-down). * * @returns the corresonding angle in the range [-180, +180] degrees */ static float computeAngle(float pitch, float roll) { return (float) Math.toDegrees(Math.atan2(Math.toRadians(pitch), // invert roll to correct sign -Math.toRadians(roll))); } /** * Magnitude property getter method (read-only property). Specifically, this * returns a number between 0 and 1, indicating how much the device * is tilted. For the angle of tilt, use {@link #Angle()}. * * <p>To return meaningful values the sensor must be enabled.</p> * * @return the magnitude of the tilt, from 0 to 1 */ @SimpleProperty(category = PropertyCategory.BEHAVIOR) public float Magnitude() { // Limit pitch and roll to 90; otherwise, the phone is upside down. // The official documentation falsely claims that the range of pitch and // roll is [-90, 90]. If the device is upside-down, it can range from // -180 to 180. We restrict it to the range [-90, 90]. // With that restriction, if the pitch and roll angles are P and R, then // the force is given by 1 - cos(P)cos(R). I have found a truly wonderful // proof of this theorem, but the margin enforced by Lint is too small to // contain it. final int MAX_VALUE = 90; double npitch = Math.toRadians(Math.min(MAX_VALUE, Math.abs(pitch))); double nroll = Math.toRadians(Math.min(MAX_VALUE, Math.abs(roll))); return (float) (1.0 - Math.cos(npitch) * Math.cos(nroll)); } // SensorListener implementation /* * Returns the rotation of the screen from its "natural" orientation. * Note that this is the angle of rotation of the drawn graphics on the * screen, which is the opposite direction of the physical rotation of the * device. For example, if the device is rotated 90 degrees counter-clockwise, * to compensate rendering will be rotated by 90 degrees clockwise and thus * the returned value here will be Surface.ROTATION_90. Return values will * be in the set Surface.ROTATION_{0,90,180,270}. */ private int getScreenRotation() { Display display = ((WindowManager) form.getSystemService(Context.WINDOW_SERVICE)). getDefaultDisplay(); if (SdkLevel.getLevel() >= SdkLevel.LEVEL_FROYO) { return FroyoUtil.getRotation(display); } else { return display.getOrientation(); } } /** * Responds to changes in the accelerometer or magnetic field sensors to * recompute orientation. This only updates azimuth, pitch, and roll and * raises the OrientationChanged event if both sensors have reported in * at least once. * * @param sensorEvent an event from the accelerometer or magnetic field sensor */ @Override public void onSensorChanged(SensorEvent sensorEvent) { if (enabled) { int eventType = sensorEvent.sensor.getType(); // Save the new sensor information about acceleration or the magnetic field. switch (eventType) { case Sensor.TYPE_ACCELEROMETER: // Update acceleration array. System.arraycopy(sensorEvent.values, 0, accels, 0, DIMENSIONS); accelsFilled = true; // Only update the accuracy property for the accelerometer. accuracy = sensorEvent.accuracy; break; case Sensor.TYPE_MAGNETIC_FIELD: // Update magnetic field array. System.arraycopy(sensorEvent.values, 0, mags, 0, DIMENSIONS); magsFilled = true; break; default: Log.e(LOG_TAG, "Unexpected sensor type: " + eventType); return; } // If we have both acceleration and magnetic information, recompute values. if (accelsFilled && magsFilled) { SensorManager.getRotationMatrix(rotationMatrix, // output inclinationMatrix, // output accels, mags); SensorManager.getOrientation(rotationMatrix, values); // Make sure values are in expected range. azimuth = OrientationSensorUtil.normalizeAzimuth( (float) Math.toDegrees(values[AZIMUTH])); pitch = OrientationSensorUtil.normalizePitch( (float) Math.toDegrees(values[PITCH])); // Sign change for roll is for compatibility with earlier versions // of App Inventor that got orientation sensor information differently. roll = OrientationSensorUtil.normalizeRoll( (float) -Math.toDegrees(values[ROLL])); // Adjust pitch and roll for phone rotation (e.g., landscape) int rotation = getScreenRotation(); switch(rotation) { case Surface.ROTATION_0: // normal rotation break; case Surface.ROTATION_90: // phone is turned 90 degrees counter-clockwise float temp = -pitch; pitch = -roll; roll = temp; break; case Surface.ROTATION_180: // phone is rotated 180 degrees roll = -roll; break; case Surface.ROTATION_270: // phone is turned 90 degrees clockwise temp = pitch; pitch = roll; roll = temp; break; default: Log.e(LOG_TAG, "Illegal value for getScreenRotation(): " + rotation); break; } // Raise event. OrientationChanged(azimuth, pitch, roll); } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // TODO(markf): Figure out if we actually need to do something here. } // Deleteable implementation @Override public void onDelete() { stopListening(); } // OnPauseListener implementation public void onPause() { stopListening(); } // OnResumeListener implementation public void onResume() { if (enabled) { startListening(); } } }