package gestures;
import gestures.detectors.DummyDetector;
import gestures.detectors.FullTurnDetector;
import gestures.detectors.LoggingDetector;
import gestures.detectors.LookingDetector;
import gestures.detectors.SlashDetector;
import gestures.detectors.UppercutDetector;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
/**
* The PhoneGestureSensor is a sensor which sits on top of the raw hardware
* sensors and detects special gestures that are made with the phone. Gestures
* are detected by choosing the most probable one at every point in time. You
* can extend the sensor's abilities by adding detector classes implementing the
* {@link PhoneGestureDetector} interface. The sensor notifies all its listeners
* whenever the most probable gesture changes to a new type.
*
* Create new instances using the PhoneGestureSensor.Builder.
*
* @author marmat (Martin Matysiak)
*/
public class PhoneGestureSensor implements SensorEventListener {
/**
* A builder to create new instances of the PhoneGestureSensor.
*
* @author marmat (Martin Matysiak)
*/
public static class Builder {
private PhoneGestureSensor sensor;
/**
* Creates a new builder instance.
*
* @param context
* The context in which to run the sensor.
*/
public Builder(Context context) {
sensor = new PhoneGestureSensor(context);
}
/**
* Sets the minimal amount of time that will be waited between to sensor
* events.
*
* @param timeout
* The timeout in milliseconds.
* @return The builder instance for method chaining.
*/
public Builder setGestureTimeout(long timeout) {
sensor.gestureTimeout = timeout;
return this;
}
/**
* Adds a dummy detector such that the NONE gesture will be detected
* whenever there is no other gesture with a probability higher than the
* given threshold.
*
* @param confidence
* The minimal needed confidence for a gesture to be
* detected.
* @return The builder instance for method chaining.
*/
public Builder withMinimumConfidence(float confidence) {
sensor.addDetector(new DummyDetector(StandardPhoneGesture.NONE, confidence));
return this;
}
/**
* Adds a detector for the SLASH gesture.
*
* @return The builder instance for method chaining.
*/
public Builder withSlashDetection() {
sensor.addDetector(new SlashDetector());
return this;
}
/**
* Adds a detector for the LOOKING gesture.
*
* @return The builder instance for method chaining.
*/
public Builder withLookingDetection() {
sensor.addDetector(new LookingDetector());
return this;
}
/**
* Adds a detector for the UPPERCUT gesture.
*
* @return The builder instance for method chaining.
*/
public Builder withUppercutDetection() {
sensor.addDetector(new UppercutDetector());
return this;
}
/**
* Adds a detector for the FULL_TURN gesture.
*
* @return The builder instance for method chaining.
*/
public Builder withFullTurnDetection() {
sensor.addDetector(new FullTurnDetector());
return this;
}
/**
* Will cause the measured values to be printed to the debug log
* whenever a sensor event occurs.
*
* @return The builder instance for method chaining.
*/
public Builder withLogging() {
sensor.addDetector(new LoggingDetector(LoggingDetector.LOG_ALL));
return this;
}
/**
* Will cause the measured values to be printed to the debug log
* whenever a sensor event occurs.
*
* @param logMask
* A bitmask specifying the messages to log. See
* {@link LoggingDetector}.
* @return The builder instance for method chaining.
*/
public Builder withLogging(int logMask) {
sensor.addDetector(new LoggingDetector(logMask));
return this;
}
/**
* Adds an arbitrary detector to the currently built sensor.
*
* @param detector
* The detector to add.
* @return The builder instance for method chaining.
*/
public Builder withDetector(PhoneGestureDetector detector) {
sensor.addDetector(detector);
return this;
}
public PhoneGestureSensor build() {
sensor.resume();
return sensor;
}
}
/**
* A list of registered event listeners.
*/
private final List<PhoneGestureListener> phoneGestureListeners;
/**
* A list of gesture detectors that will be used to choose the most probable
* gesture.
*/
private final List<PhoneGestureDetector> phoneGestureDetectors;
/**
* The SensorManager to which this sensor is connected to.
*/
private final SensorManager sensorManager;
/**
* Timestamp of the last propagated event in milliseconds.
*/
private long lastEvent;
/**
* The last propagated gesture.
*/
private PhoneGesture lastGesture = StandardPhoneGesture.NONE;
/**
* The gravity vector indicating the measured values of gravity along the
* phone's axes.
*/
private float[] gravity = { 0, 0, 0 };
/**
* The weight for the high pass filter when filtering out gravity.
*/
private final float alpha = 0.8f;
/**
* The minimal amount of time between to gestures in milliseconds.
*/
public long gestureTimeout = 500;
/**
* Creates a new sensor instance that will be looking for phone gestures.
*
* @param context
* The application context in which to run.
*/
private PhoneGestureSensor(Context context) {
phoneGestureListeners = new ArrayList<PhoneGestureListener>();
phoneGestureDetectors = new ArrayList<PhoneGestureDetector>();
sensorManager = (SensorManager) context
.getSystemService(Context.SENSOR_SERVICE);
}
/**
* Registers the given listener with this sensor, or does nothing if the
* given listener is already registered.
*
* @param listener
* The listener to register.
*/
public void addListener(PhoneGestureListener listener) {
if (!phoneGestureListeners.contains(listener)) {
phoneGestureListeners.add(listener);
}
}
/**
* Removes the given listener from events of this sensor, or does nothing if
* the listener wasn't previously registered.
*
* @param listener
* The listener to remove.
*/
public void removeListener(PhoneGestureListener listener) {
phoneGestureListeners.remove(listener);
}
/**
* Adds the given detector to this sensor. The detector will then receive
* sensor events as long as this sensor is active.
*
* @param detector
* The detector to add.
*/
public void addDetector(PhoneGestureDetector detector) {
if (!phoneGestureDetectors.contains(detector)) {
phoneGestureDetectors.add(detector);
}
}
/**
* Removes the given detector if it was previously registered to this
* sensor.
*
* @param detector
* The detector to remove.
*/
public void removeDetector(PhoneGestureDetector detector) {
phoneGestureDetectors.remove(detector);
}
/**
* Removes effects of gravity or offset and returns a vector with pure
* linear acceleration.
*
* @param event
* The SensorEvent that occurred.
* @return The linear acceleration vector along the phone's axes.
*/
private float[] getLinearAcceleration(SensorEvent event) {
float[] result = { 0, 0, 0 };
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
// High pass filtering to remove gravity (see also goo.gl/dl85N)
gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];
result[0] = event.values[0] - gravity[0];
result[1] = event.values[1] - gravity[1];
result[2] = event.values[2] - gravity[2];
break;
// TODO: Add support for Sensor.TYPE_LINEAR_ACCELERATION
}
return result;
}
/**
* Gets the current readings of the magnetic field sensor.
*
* @param event
* The SensorEvent that occurred.
* @return The readings of the magnetic sensor.
*/
private float[] getMagneticField(SensorEvent event) {
float[] result = { 0, 0, 0 };
switch (event.sensor.getType()) {
case Sensor.TYPE_MAGNETIC_FIELD:
result[0] = event.values[0];
result[1] = event.values[1];
result[2] = event.values[2];
}
return result;
}
/**
* Notifies all registered listeners of the event that occurred.
*
* @param phoneGesture
* The gesture that has been detected.
*/
private void propagateEvent(PhoneGesture phoneGesture) {
long currentTime = System.currentTimeMillis();
if ((lastGesture != StandardPhoneGesture.NONE && currentTime - lastEvent < gestureTimeout)
|| lastGesture == phoneGesture) {
return;
}
lastEvent = currentTime;
lastGesture = phoneGesture;
// We copy the current list of listeners into an array to achieve
// thread safety since new listeners could be registered while
// propagating the current event.
PhoneGestureListener[] listenersToCall = new PhoneGestureListener[phoneGestureListeners
.size()];
listenersToCall = phoneGestureListeners.toArray(listenersToCall);
for (PhoneGestureListener listener : listenersToCall) {
listener.onPhoneGesture(phoneGesture);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO: check if we should handle this
}
@Override
public void onSensorChanged(SensorEvent event) {
float[] linearAcceleration = getLinearAcceleration(event);
float[] mag = getMagneticField(event);
SensorData data = new SensorData(gravity, linearAcceleration, mag);
PhoneGestureDetector mostProbableDetector = null;
for (PhoneGestureDetector detector : phoneGestureDetectors) {
detector.feedSensorEvent(data);
if (mostProbableDetector == null
|| detector.getProbability() > mostProbableDetector
.getProbability()) {
mostProbableDetector = detector;
}
}
propagateEvent(mostProbableDetector != null ? mostProbableDetector
.getType() : StandardPhoneGesture.NONE);
}
/**
* Pauses detection of gestures for all registered listeners. Use with
* caution.
*/
public void pause() {
sensorManager.unregisterListener(this);
}
/**
* Resumes detection of gestures.
*/
public void resume() {
sensorManager.registerListener(this,
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(this,
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_GAME);
}
/**
* Stops listening to events once and for all. Use with caution! Should be
* only called when it's completely sure that the user won't return to the
* activity.
*/
public void stop() {
sensorManager.unregisterListener(this);
phoneGestureListeners.clear();
}
}