diff --git a/jme3-android-examples/src/main/AndroidManifest.xml b/jme3-android-examples/src/main/AndroidManifest.xml
index d41aba8ce..cf9cc7c2d 100644
--- a/jme3-android-examples/src/main/AndroidManifest.xml
+++ b/jme3-android-examples/src/main/AndroidManifest.xml
@@ -45,5 +45,6 @@
+
diff --git a/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java b/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java
new file mode 100644
index 000000000..3805435d9
--- /dev/null
+++ b/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java
@@ -0,0 +1,314 @@
+package jme3test.android;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.Joystick;
+import com.jme3.input.JoystickAxis;
+import com.jme3.input.MouseInput;
+import com.jme3.input.SensorJoystickAxis;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Line;
+import com.jme3.texture.Texture;
+import com.jme3.util.IntMap;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Example Test Case to test using Android sensors as Joystick axes. Make sure to enable Joystick Events from
+ * the test chooser menus. Rotating the device will cause the block to rotate. Tapping the screen will cause the
+ * sensors to be calibrated (reset to zero) at the current orientation. Continuously tapping the screen causes
+ * the "rumble" to intensify until it reaches the maximum amount and then it shuts off.
+ *
+ * @author iwgeric
+ */
+public class TestAndroidSensors extends SimpleApplication implements ActionListener, AnalogListener {
+
+ private static final Logger logger = Logger.getLogger(TestAndroidSensors.class.getName());
+
+ private Geometry geomZero = null;
+ // Map of joysticks saved with the joyId as the key
+ private IntMap joystickMap = new IntMap();
+ // flag to allow for the joystick axis to be calibrated on startup
+ private boolean initialCalibrationComplete = false;
+ // mappings used for onAnalog
+ private final String ORIENTATION_X_PLUS = "Orientation_X_Plus";
+ private final String ORIENTATION_X_MINUS = "Orientation_X_Minus";
+ private final String ORIENTATION_Y_PLUS = "Orientation_Y_Plus";
+ private final String ORIENTATION_Y_MINUS = "Orientation_Y_Minus";
+ private final String ORIENTATION_Z_PLUS = "Orientation_Z_Plus";
+ private final String ORIENTATION_Z_MINUS = "Orientation_Z_Minus";
+
+
+ // variables to save the current rotation
+ // Used when controlling the geometry with device orientation
+ private float[] anglesCurrent = new float[]{0f, 0f, 0f};
+ private Quaternion rotationQuat = new Quaternion();
+
+ // switch to apply an absolute rotation (geometry.setLocalRotation) or
+ // an incremental constant rotation (geometry.rotate)
+ // Used when controlling the geometry with device orientation
+ private boolean useAbsolute = false;
+
+ // rotation speed to use when apply constant incremental rotation
+ // Used when controlling the geometry with device orientation
+ private float rotationSpeedX = 1f;
+ private float rotationSpeedY = 1f;
+
+ // current intensity of the rumble
+ float rumbleAmount = 0f;
+
+ // toggle to enable rumble
+ boolean enableRumble = true;
+
+ // toggle to enable device orientation in FlyByCamera
+ boolean enableFlyByCameraRotation = false;
+
+ // toggle to enable controlling geometry rotation
+ boolean enableGeometryRotation = true;
+
+ // Make sure to set joystickEventsEnabled = true in MainActivity for Android
+
+ private float toDegrees(float rad) {
+ return rad * FastMath.RAD_TO_DEG;
+ }
+
+ @Override
+ public void simpleInitApp() {
+
+ // useAbsolute = true;
+ // enableRumble = true;
+
+ if (enableFlyByCameraRotation) {
+ flyCam.setEnabled(true);
+ } else {
+ flyCam.setEnabled(false);
+ }
+
+ Mesh lineX = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_X.mult(3)));
+ Mesh lineY = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_Y.mult(3)));
+ Mesh lineZ = new Line(Vector3f.ZERO, Vector3f.ZERO.add(Vector3f.UNIT_Z.mult(3)));
+
+ Geometry geoX = new Geometry("X", lineX);
+ Material matX = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ matX.setColor("Color", ColorRGBA.Red);
+ matX.getAdditionalRenderState().setLineWidth(30);
+ geoX.setMaterial(matX);
+ rootNode.attachChild(geoX);
+
+ Geometry geoY = new Geometry("Y", lineY);
+ Material matY = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ matY.setColor("Color", ColorRGBA.Green);
+ matY.getAdditionalRenderState().setLineWidth(30);
+ geoY.setMaterial(matY);
+ rootNode.attachChild(geoY);
+
+ Geometry geoZ = new Geometry("Z", lineZ);
+ Material matZ = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ matZ.setColor("Color", ColorRGBA.Blue);
+ matZ.getAdditionalRenderState().setLineWidth(30);
+ geoZ.setMaterial(matZ);
+ rootNode.attachChild(geoZ);
+
+ Box b = new Box(1, 1, 1);
+ geomZero = new Geometry("Box", b);
+ Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", ColorRGBA.Yellow);
+ Texture tex_ml = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
+ mat.setTexture("ColorMap", tex_ml);
+ geomZero.setMaterial(mat);
+ geomZero.setLocalTranslation(Vector3f.ZERO);
+ geomZero.setLocalRotation(Quaternion.IDENTITY);
+ rootNode.attachChild(geomZero);
+
+
+ // Touch (aka MouseInput.BUTTON_LEFT) is used to record the starting
+ // orientation when using absolute rotations
+ inputManager.addMapping("MouseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+ inputManager.addListener(this, "MouseClick");
+
+ Joystick[] joysticks = inputManager.getJoysticks();
+ if (joysticks == null || joysticks.length < 1) {
+ logger.log(Level.INFO, "Cannot find any joysticks!");
+ } else {
+
+ // Joysticks return a value of 0 to 1 based on how far the stick is
+ // push on the axis. This value is then scaled based on how long
+ // during the frame the joystick axis has been in that position.
+ // If the joystick is push all the way for the whole frame,
+ // then the value in onAnalog is equal to tpf.
+ // If the joystick is push 1/2 way for the entire frame, then the
+ // onAnalog value is 1/2 tpf.
+ // Similarly, if the joystick is pushed to the maximum during a frame
+ // the value in onAnalog will also be scaled.
+ // For Android sensors, rotating the device 90deg is the same as
+ // pushing an actual joystick axis to the maximum.
+
+ logger.log(Level.INFO, "Number of joysticks: {0}", joysticks.length);
+ JoystickAxis axis;
+
+ for (Joystick joystick : joysticks) {
+ // Get and display all axes in joystick.
+ List axes = joystick.getAxes();
+ for (JoystickAxis joystickAxis : axes) {
+ logger.log(Level.INFO, "{0} axis scan Name: {1}, LogicalId: {2}, AxisId: {3}",
+ new Object[]{joystick.getName(), joystickAxis.getName(), joystickAxis.getLogicalId(), joystickAxis.getAxisId()});
+ }
+
+ // Get specific axis based on LogicalId of the JoystickAxis
+ // If found, map axis
+ axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_X);
+ if (axis != null) {
+ axis.assignAxis(ORIENTATION_X_PLUS, ORIENTATION_X_MINUS);
+ inputManager.addListener(this, ORIENTATION_X_PLUS, ORIENTATION_X_MINUS);
+ logger.log(Level.INFO, "Found {0} Joystick, assigning mapping for X axis: {1}, with max value: {2}",
+ new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis) axis).getMaxRawValue()});
+ }
+
+ axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_Y);
+ if (axis != null) {
+ axis.assignAxis(ORIENTATION_Y_PLUS, ORIENTATION_Y_MINUS);
+ inputManager.addListener(this, ORIENTATION_Y_PLUS, ORIENTATION_Y_MINUS);
+ logger.log(Level.INFO, "Found {0} Joystick, assigning mapping for Y axis: {1}, with max value: {2}",
+ new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis) axis).getMaxRawValue()});
+ }
+
+ axis = joystick.getAxis(SensorJoystickAxis.ORIENTATION_Z);
+ if (axis != null) {
+ axis.assignAxis(ORIENTATION_Z_PLUS, ORIENTATION_Z_MINUS);
+ inputManager.addListener(this, ORIENTATION_Z_PLUS, ORIENTATION_Z_MINUS);
+ logger.log(Level.INFO, "Found {0} Joystick, assigning mapping for Z axis: {1}, with max value: {2}",
+ new Object[]{joystick.toString(), axis.toString(), ((SensorJoystickAxis) axis).getMaxRawValue()});
+ }
+
+ joystickMap.put(joystick.getJoyId(), joystick);
+
+ }
+ }
+ }
+
+
+ @Override
+ public void simpleUpdate(float tpf) {
+ if (!initialCalibrationComplete) {
+ // Calibrate the axis (set new zero position) if the axis
+ // is a sensor joystick axis
+ for (IntMap.Entry entry : joystickMap) {
+ for (JoystickAxis axis : entry.getValue().getAxes()) {
+ if (axis instanceof SensorJoystickAxis) {
+ logger.log(Level.INFO, "Calibrating Axis: {0}", axis.toString());
+ ((SensorJoystickAxis) axis).calibrateCenter();
+ }
+ }
+ }
+ initialCalibrationComplete = true;
+ }
+
+ if (enableGeometryRotation) {
+ rotationQuat.fromAngles(anglesCurrent);
+ rotationQuat.normalizeLocal();
+
+ if (useAbsolute) {
+ geomZero.setLocalRotation(rotationQuat);
+ } else {
+ geomZero.rotate(rotationQuat);
+ }
+
+ anglesCurrent[0] = anglesCurrent[1] = anglesCurrent[2] = 0f;
+ }
+ }
+
+
+ public void onAction(String string, boolean pressed, float tpf) {
+ if (string.equalsIgnoreCase("MouseClick") && pressed) {
+ // Calibrate the axis (set new zero position) if the axis
+ // is a sensor joystick axis
+ for (IntMap.Entry entry : joystickMap) {
+ for (JoystickAxis axis : entry.getValue().getAxes()) {
+ if (axis instanceof SensorJoystickAxis) {
+ logger.log(Level.INFO, "Calibrating Axis: {0}", axis.toString());
+ ((SensorJoystickAxis) axis).calibrateCenter();
+ }
+ }
+ }
+
+ if (enableRumble) {
+ // manipulate joystick rumble
+ for (IntMap.Entry entry : joystickMap) {
+ rumbleAmount += 0.1f;
+ if (rumbleAmount > 1f + FastMath.ZERO_TOLERANCE) {
+ rumbleAmount = 0f;
+ }
+ logger.log(Level.INFO, "rumbling with amount: {0}", rumbleAmount);
+ entry.getValue().rumble(rumbleAmount);
+ }
+ }
+ }
+ }
+
+
+ public void onAnalog(String string, float value, float tpf) {
+ logger.log(Level.INFO, "onAnalog for {0}, value: {1}, tpf: {2}",
+ new Object[]{string, value, tpf});
+ float scaledValue = value;
+
+ if (string.equalsIgnoreCase(ORIENTATION_X_PLUS)) {
+ if (useAbsolute) {
+ // set rotation amount
+ // divide by tpf to get back to actual axis value (0 to 1)
+ // multiply by 90deg so that 90deg = full axis (value = tpf)
+ anglesCurrent[0] = (scaledValue / tpf * FastMath.HALF_PI);
+ } else {
+ // apply an incremental rotation amount based on rotationSpeed
+ anglesCurrent[0] += scaledValue * rotationSpeedX;
+ }
+ }
+
+ if (string.equalsIgnoreCase(ORIENTATION_X_MINUS)) {
+ if (useAbsolute) {
+ // set rotation amount
+ // divide by tpf to get back to actual axis value (0 to 1)
+ // multiply by 90deg so that 90deg = full axis (value = tpf)
+ anglesCurrent[0] = (-scaledValue / tpf * FastMath.HALF_PI);
+ } else {
+ // apply an incremental rotation amount based on rotationSpeed
+ anglesCurrent[0] -= scaledValue * rotationSpeedX;
+ }
+ }
+
+ if (string.equalsIgnoreCase(ORIENTATION_Y_PLUS)) {
+ if (useAbsolute) {
+ // set rotation amount
+ // divide by tpf to get back to actual axis value (0 to 1)
+ // multiply by 90deg so that 90deg = full axis (value = tpf)
+ anglesCurrent[1] = (scaledValue / tpf * FastMath.HALF_PI);
+ } else {
+ // apply an incremental rotation amount based on rotationSpeed
+ anglesCurrent[1] += scaledValue * rotationSpeedY;
+ }
+ }
+
+ if (string.equalsIgnoreCase(ORIENTATION_Y_MINUS)) {
+ if (useAbsolute) {
+ // set rotation amount
+ // divide by tpf to get back to actual axis value (0 to 1)
+ // multiply by 90deg so that 90deg = full axis (value = tpf)
+ anglesCurrent[1] = (-scaledValue / tpf * FastMath.HALF_PI);
+ } else {
+ // apply an incremental rotation amount based on rotationSpeed
+ anglesCurrent[1] -= scaledValue * rotationSpeedY;
+ }
+ }
+
+ }
+}
\ No newline at end of file