// -*- 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.SimpleFunction;
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 android.content.Context;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;
/**
* This component keeps count of steps using the accelerometer.
*
*/
@DesignerComponent(version = YaVersion.PEDOMETER_COMPONENT_VERSION,
description = "A Component that acts like a Pedometer. It senses motion via the " +
"Accerleromter and attempts to determine if a step has been " +
"taken. Using a configurable stride length, it can estimate the " +
"distance traveled as well. ",
category = ComponentCategory.SENSORS,
nonVisible = true,
iconName = "images/pedometer.png")
@SimpleObject
public class Pedometer extends AndroidNonvisibleComponent
implements Component, SensorEventListener, Deleteable {
private static final String TAG = "Pedometer";
private static final String PREFS_NAME = "PedometerPrefs";
private static final int INTERVAL_VARIATION = 250;
private static final int NUM_INTERVALS = 2;
private static final int WIN_SIZE = 100;
private static final float STRIDE_LENGTH = (float) 0.73;
private static final float PEAK_VALLEY_RANGE = (float) 40.0;
private final Context context;
private final SensorManager sensorManager;
private int stopDetectionTimeout = 2000;
private int winPos = 0, intervalPos = 0;
private int numStepsWithFilter = 0, numStepsRaw = 0;
private float lastValley = 0;
private float[] lastValues = new float[WIN_SIZE];
private float strideLength = STRIDE_LENGTH;
private float totalDistance = 0;
private long[] stepInterval = new long[NUM_INTERVALS];
private long stepTimestamp = 0;
private long startTime = 0, prevStopClockTime = 0;
private boolean foundValley = false;
private boolean startPeaking = false;
private boolean foundNonStep = true;
private boolean pedometerPaused = true;
private float[] avgWindow = new float[10];
private int avgPos = 0;
/** Constructor. */
public Pedometer(ComponentContainer container) {
super(container.$form());
context = container.$context();
// some initialization
winPos = 0;
startPeaking = false;
numStepsWithFilter = 0;
numStepsRaw = 0;
foundValley = true;
lastValley = 0;
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
// Restore preferences
SharedPreferences settings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
strideLength = settings.getFloat("Pedometer.stridelength", STRIDE_LENGTH);
totalDistance = settings.getFloat("Pedometer.distance", 0);
numStepsRaw = settings.getInt("Pedometer.prevStepCount", 0);
prevStopClockTime = settings.getLong("Pedometer.clockTime", 0);
numStepsWithFilter = numStepsRaw;
startTime = System.currentTimeMillis();
Log.d(TAG, "Pedometer Created");
}
// Simple functions
/**
* Starts the pedometer.
*/
@SimpleFunction(description = "Start counting steps")
public void Start() {
if (pedometerPaused) {
pedometerPaused = false;
sensorManager.registerListener(this,
sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0),
SensorManager.SENSOR_DELAY_FASTEST);
startTime = System.currentTimeMillis();
}
}
/**
* Stops the pedometer.
*/
@SimpleFunction(description = "Stop counting steps")
public void Stop() {
Pause();
}
/**
* Resets the step count, distance, and clock.
*/
@SimpleFunction(description = "Resets the step counter, distance measure and time running.")
public void Reset() {
numStepsWithFilter = 0;
numStepsRaw = 0;
totalDistance = 0;
prevStopClockTime = 0;
startTime = System.currentTimeMillis();
}
/**
* Resumes the counting of steps.
*/
@SimpleFunction(description = "Resumes counting, synonym of Start.")
public void Resume() {
Start();
}
/**
* Pauses the counting of steps.
*/
@SimpleFunction(description = "Pause counting of steps and distance.")
public void Pause() {
if (!pedometerPaused) {
pedometerPaused = true;
sensorManager.unregisterListener(this);
Log.d(TAG, "Unregistered listener on pause");
prevStopClockTime += (System.currentTimeMillis() - startTime);
}
}
/**
* Saves the pedometer state to shared preferences.
*/
@SimpleFunction(description = "Saves the pedometer state to the phone. Permits " +
"permits accumulation of steps and distance between invocations of an App that uses " +
"the pedometer. Different Apps will have their own saved state.")
public void Save() {
// Store preferences
SharedPreferences settings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putFloat("Pedometer.stridelength", strideLength);
editor.putFloat("Pedometer.distance", totalDistance);
editor.putInt("Pedometer.prevStepCount", numStepsRaw);
if (pedometerPaused) {
editor.putLong("Pedometer.clockTime", prevStopClockTime);
} else {
editor.putLong("Pedometer.clockTime", prevStopClockTime +
(System.currentTimeMillis() - startTime));
}
editor.putLong("Pedometer.closeTime", System.currentTimeMillis());
editor.commit();
Log.d(TAG, "Pedometer state saved.");
}
// Events
/**
* Indicates that a step was taken.
*
* @param simpleSteps number of raw steps detected
* @param distance approximate distance covered by number of simpleSteps in meters
*/
@SimpleEvent(description = "This event is run when a raw step is detected")
public void SimpleStep(int simpleSteps, float distance) {
EventDispatcher.dispatchEvent(this, "SimpleStep", simpleSteps, distance);
}
/**
* Indicates that a step was taken while walking. This will not be called if
* a single step is taken while standing still.
*
* @param walkSteps number of walking steps detected
* @param distance approximate distance covered by the number of walkSteps in meters
*/
@SimpleEvent(description = "This event is run when a walking step is detected. " +
"A walking step is a step that appears to be involved in forward motion.")
public void WalkStep(int walkSteps, float distance) {
EventDispatcher.dispatchEvent(this, "WalkStep", walkSteps, distance);
}
// Properties
/**
* Specifies the stride length in meters. The application can use this to explicitly set
* stride length to override the one calculated by the pedometer's calibration mechanism.
* As a side effect, this method turns off calibration of stride length using the GPS.
*
* @param length is the stride length in meters.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_FLOAT,
defaultValue = "0.73")
@SimpleProperty(
description = "Set the average stride length in meters.",
category = PropertyCategory.BEHAVIOR)
public void StrideLength(float length) {
strideLength = length;
}
/**
* Returns the current estimate of stride length in meters, if calibrated, or returns the
* default (0.73 m) otherwise.
*
* @return length of the stride in meters.
*/
@SimpleProperty
public float StrideLength() {
return strideLength;
}
/**
* Sets the duration of idleness (no steps detected) after which to go into a "stopped" state.
*
* @param timeout the timeout in milliseconds.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER,
defaultValue = "2000")
@SimpleProperty(
category = PropertyCategory.BEHAVIOR,
description = "The duration in milliseconds of idleness (no steps detected) " +
"after which to go into a \"stopped\" state")
public void StopDetectionTimeout(int timeout) {
stopDetectionTimeout = timeout;
}
/**
* Returns the duration of idleness (no steps detected) after which to go into a "stopped" state.
*
* @return the timeout in milliseconds.
*/
@SimpleProperty
public int StopDetectionTimeout() {
return stopDetectionTimeout;
}
/**
* Returns the approximate distance traveled in meters.
*
* @return approximate distance traveled in meters.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR, description = "The approximate distance traveled in meters.")
public float Distance() {
return totalDistance;
}
/**
* Returns the time elapsed in milliseconds since the pedometer has started.
*
* @return time elapsed in milliseconds since the pedometer was started.
*/
@SimpleProperty(
category = PropertyCategory.BEHAVIOR, description = "Time elapsed in milliseconds since the pedometer was started.")
public long ElapsedTime() {
if (pedometerPaused) {
return prevStopClockTime;
} else {
return prevStopClockTime + (System.currentTimeMillis() - startTime);
}
}
/**
* Returns the number of simple steps taken since the pedometer has started.
*
* @return the number of simple steps since the pedometer was started.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "The number of simple steps taken since the pedometer has started.")
public int SimpleSteps() {
return numStepsRaw;
}
/**
* Returns the number of walk steps taken since the pedometer has started.
*
* @return the number of walk steps since the pedometer was started.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "the number of walk steps taken since the pedometer has started.")
public int WalkSteps() {
return numStepsWithFilter;
}
/**
* Finds average of the last NUM_INTERVALS number of step intervals
* and checks if they are roughly equally spaced.
*/
private boolean areStepsEquallySpaced() {
float avg = 0;
int num = 0;
for (long interval : stepInterval) {
if (interval > 0) {
num++;
avg += interval;
}
}
avg = avg / num;
for (long interval : stepInterval) {
if (Math.abs(interval - avg) > INTERVAL_VARIATION) {
return false;
}
}
return true;
}
/**
* Checks if the current middle of the window is the local peak.
*/
private boolean isPeak() {
int mid = (winPos + WIN_SIZE / 2) % WIN_SIZE;
for (int i = 0; i < WIN_SIZE; i++) {
if (i != mid && lastValues[i] > lastValues[mid]) {
return false;
}
}
return true;
}
/**
* Checks if the current middle of the window is the local peak.
*/
private boolean isValley() {
int mid = (winPos + WIN_SIZE / 2) % WIN_SIZE;
for (int i = 0; i < WIN_SIZE; i++) {
if (i != mid && lastValues[i] < lastValues[mid]) {
return false;
}
}
return true;
}
// SensorEventListener implementation
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.d(TAG, "Accelerometer accuracy changed.");
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
return;
}
float[] values = event.values;
float magnitude = 0;
for (float v : values) magnitude += v * v;
// Check if the middle reading within the current window represents
// a peak/valley.
int mid = (winPos + WIN_SIZE / 2) % WIN_SIZE;
// Peak is detected
if (startPeaking && isPeak()) {
if (foundValley && lastValues[mid] - lastValley > PEAK_VALLEY_RANGE) {
// Step detected on axis k with maximum peak-valley range.
long timestamp = System.currentTimeMillis();
stepInterval[intervalPos] = timestamp - stepTimestamp;
intervalPos = (intervalPos + 1) % NUM_INTERVALS;
stepTimestamp = timestamp;
if (areStepsEquallySpaced()) {
if (foundNonStep) {
numStepsWithFilter += NUM_INTERVALS;
totalDistance += strideLength * NUM_INTERVALS;
foundNonStep = false;
}
numStepsWithFilter++;
WalkStep(numStepsWithFilter, totalDistance);
totalDistance += strideLength;
} else {
foundNonStep = true;
}
numStepsRaw++;
SimpleStep(numStepsRaw, totalDistance);
foundValley = false;
}
}
// Valley is detected
if (startPeaking && isValley()) {
foundValley = true;
lastValley = lastValues[mid];
}
// Store latest accelerometer reading in the window.
avgWindow[avgPos] = magnitude;
avgPos = (avgPos + 1) % avgWindow.length;
lastValues[winPos] = 0;
for (float m : avgWindow) lastValues[winPos] += m;
lastValues[winPos] /= avgWindow.length;
if (startPeaking || winPos > 1) {
int i = winPos;
if (--i < 0) i += WIN_SIZE;
lastValues[winPos] += 2 * lastValues[i];
if (--i < 0) i += WIN_SIZE;
lastValues[winPos] += lastValues[i];
lastValues[winPos] /= 4;
} else if (!startPeaking && winPos == 1) {
lastValues[1] = (lastValues[1] + lastValues[0]) / 2f;
}
long elapsedTimestamp = System.currentTimeMillis();
if (elapsedTimestamp - stepTimestamp > stopDetectionTimeout) {
stepTimestamp = elapsedTimestamp;
}
// Once the buffer is full, start peak/valley detection.
if (winPos == WIN_SIZE - 1 && !startPeaking) {
startPeaking = true;
}
// Increment position within the window.
winPos = (winPos + 1) % WIN_SIZE;
}
// Deleteable implementation
@Override
public void onDelete() {
sensorManager.unregisterListener(this);
}
// DEPRECATED:
// Everything below here is deprecated. We cannot completely remove them
// because older projects loaded into the system that use them would break.
// Instead we leave stub routines which are annotated as @Deprecated. This
// will result in blocks for these routines to be marked bad when a project
// that has them is loaded.
@Deprecated
@SimpleEvent
public void StartedMoving() {
}
@Deprecated
@SimpleEvent
public void StoppedMoving() {
}
@Deprecated
@SimpleProperty(category = PropertyCategory.BEHAVIOR)
public void UseGPS(boolean gps) {
}
@Deprecated
@SimpleEvent
public void CalibrationFailed() {
}
@Deprecated
@SimpleEvent
public void GPSAvailable() {
}
@Deprecated
@SimpleEvent
public void GPSLost() {
}
// Properties
@Deprecated
@SimpleProperty(category = PropertyCategory.BEHAVIOR)
public void CalibrateStrideLength(boolean cal) {
}
@Deprecated
@SimpleProperty
public boolean CalibrateStrideLength() {
return false;
}
@Deprecated
@SimpleProperty
public boolean Moving() {
return false;
}
}