/*
* Copyright (C) 2011 The original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.zapta.apps.maniana.services;
import java.util.Arrays;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import com.zapta.apps.maniana.annotations.MainActivityScope;
import com.zapta.apps.maniana.util.LogUtil;
/**
* An implementation of ShakerDetector. This implementation computes for each accelerometer event
* the accelerator change magnitude |da=<dx, dy, dz>| where da the acceleration difference vector
* from previous acceleration event (da represents the first derivative of the acceleration). An
* shake event is generated if (da1 - da2) > threshold, where da1 (da2) is the average da over the
* last N1 (N2) events, where N1 < N2. da1 represent the recent value of da while da2 represents the
* longer term background noise level.
*
* @author Tal Dayan.
*/
@MainActivityScope
public class ShakeImpl implements Shaker {
/** Number of events in the short term time window */
private final static int N1 = 1;
/** Number of events in the longer term time window (background noise) */
private final static int N2 = 5;
private final ShakerListener mListener;
private final SensorManager mSensorManager;
private final Sensor mAccelerometer;
/** Indicates if the shaker is currently resumed or paused. */
private boolean mIsResumed = false;
/** Acceleration from previous event. */
private float mLastX;
private float mLastY;
private float mLastZ;
/** System time of previous event. */
private long mLastEventTimeMillis;
/** Magnitude from last N2 events. */
private final int[] mHistory = new int[N2];
/** Next insertion point in history, [0..N1) */
private int mNextIndex;
/** Sum of the last N1 history points. */
private int mSum1;
/** Sum of the last N2 history points. */
private int mSum2;
/** If greater than zero, do not allow a shake event for this number of sensor events. */
private int mBlackout = 0;
/** Used to report shaker's liveliness */
private long mLastLiveReportingTimeMillis;
private int mEvnetsSinceLastLiveReporting;
/** Shake event is triggered when signal > this value. Set later. */
private int mThreshold;
/** Event handling adapter. */
private final SensorEventListener mSensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent se) {
handleSensorChanged(se);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Ignored
}
};
/** Constructor. Leaves the shaker in paused state. */
public ShakeImpl(Context context, ShakerListener listner) {
this.mListener = listner;
// NOTE: this call sometimes hangs under emulator.
// See http://code.google.com/p/android/issues/detail?id=2566
// See http://stackoverflow.com/questions/8626718
LogUtil.info("Shaker: getting sensor service...");
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
LogUtil.info("Shaker: got sensor service.");
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
private final void resetHistory() {
Arrays.fill(mHistory, 0);
mNextIndex = 0;
mSum1 = 0;
mSum2 = 0;
// TODO: have a more explicit 'history full' condition.
//
// Suppress shake events until the history buffer will get refilled
mBlackout = N2 - 1;
}
private final void resetState() {
// LogUtil.debug("Reseting state");
resetHistory();
mLastX = 0f;
mLastY = 0f;
mLastZ = 0f;
mLastEventTimeMillis = 0;
mLastLiveReportingTimeMillis = System.currentTimeMillis();
mEvnetsSinceLastLiveReporting = 0;
}
/**
* Accelerometer change event. Called periodically when in resume state.
*/
private void handleSensorChanged(SensorEvent se) {
final long currentEventTimeMillis = System.currentTimeMillis();
mEvnetsSinceLastLiveReporting++;
final long reportingDeltaTimeMillis = currentEventTimeMillis - mLastLiveReportingTimeMillis;
// Report every 30 secs
if (reportingDeltaTimeMillis > (30 * 1000)) {
LogUtil.info("Shaker: %d events in %dms",
mEvnetsSinceLastLiveReporting, reportingDeltaTimeMillis);
mLastLiveReportingTimeMillis = currentEventTimeMillis;
mEvnetsSinceLastLiveReporting = 0;
}
// Accelerations in X,Y,Z direction
final float x = se.values[SensorManager.DATA_X];
final float y = se.values[SensorManager.DATA_Y];
final float z = se.values[SensorManager.DATA_Z];
// Calculate acceleration change (first derivative of acceleration vector).
final float dX = x - mLastX;
final float dY = y - mLastY;
final float dZ = z - mLastZ;
// If no previous sample than skip this event.
if (mLastEventTimeMillis == 0) {
LogUtil.info("Shaker: No prev sample, skipping this one");
mLastEventTimeMillis = currentEventTimeMillis;
return;
}
// If delta time is way too long, reset state. Sensing has paused
// for some reason.
final long deltaTimeMillis = (currentEventTimeMillis - mLastEventTimeMillis);
mLastEventTimeMillis = currentEventTimeMillis;
if (deltaTimeMillis > 5000) {
// Note: resetHistory() preserve the lastXYZ and last event time.
LogUtil.info("Shaker: Reseting history, dt: %sms", deltaTimeMillis);
resetHistory();
return;
}
// Calculate change magnitude |<dx, dy, dz>|. Scaled by an arbitrary scale
// to provide enough int bits of accuracy. We use ints to avoid accomulating
// error in the incremental tracking of sum1, sum2.
final int newValue = (int) (Math.sqrt((dX * dX) + (dY * dY) + (dZ * dZ)) * 500);
// Push to history queue and update incrementally the N1 and N2 sums.
final int droppedValue1 = mHistory[(mNextIndex + N2 - N1) % N2];
final int droppedValue2 = mHistory[mNextIndex];
mHistory[mNextIndex++] = newValue;
if (mNextIndex >= N2) {
mNextIndex = 0;
}
mSum1 += (newValue - droppedValue1);
mSum2 += (newValue - droppedValue2);
// Save for next iteration
mLastX = x;
mLastY = y;
mLastZ = z;
// If in blackout, don't issue a shake event in this cycle.
if (mBlackout > 0) {
mBlackout--;
return;
}
// The monitored signal is the difference between the short term average and the long
// term average (noise level)
final int avg1 = mSum1 / N1;
final int avg2 = mSum2 / N2;
final int signal = (avg1 - avg2);
// LogUtil.debug("signal: %s", signal);
// Compare signal to the detection threshold
if (signal > mThreshold) {
LogUtil.info("Shaker: initiaging onShake()");
mListener.onShake();
// Debouncing. Avoid successive shake event for the next N2 cycles.
mBlackout = N2;
}
}
@Override
public boolean resume(int force) {
if (!mIsResumed) {
resetState();
// Using NORMAL (low) rate for better battery life.
mIsResumed = mSensorManager.registerListener(mSensorListener, mAccelerometer,
SensorManager.SENSOR_DELAY_NORMAL);
}
// Force sensitivity to [1..10]
final int actualForce = Math.max(1, Math.min(10, force));
// Map sensitivity to threshold. Values are based on trial and error..
mThreshold = 1300 + (actualForce * 700);
// LogUtil.debug("Shaker resumed, force: %s, threshold: %s", actualForce, threshold);
return mIsResumed;
}
@Override
public void pause() {
if (mIsResumed) {
mSensorManager.unregisterListener(mSensorListener);
mIsResumed = false;
}
}
}