/* * Copyright (C) 2016 gerhard.nospam@gmail.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.runnerup.tracker.component; import android.content.Context; import android.content.SharedPreferences; import android.hardware.*; import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; import org.runnerup.BuildConfig; import org.runnerup.common.util.Constants; import java.util.HashMap; import java.util.Random; public class TrackerCadence extends DefaultTrackerComponent implements SensorEventListener { public static final String NAME = "Cadence"; @Override public String getName() { return NAME; } private SensorManager sensorManager = null; //For debug builds, use random if sensor is unavailable private final static boolean testMode = BuildConfig.DEBUG; private static boolean isEmulating = false; private boolean isSportEnabled = true; //The sensor fires continuously, use the last available values (no smoothing) private boolean isStarted = true; private Float latestVal = null; private long latestTime = -1; private Float prevVal = null; private long prevTime = -1; private Float currentCadence = null; public Float getValue() { if (isEmulating) { if (latestVal == null) {latestVal = 0.0f;} //if GPS update is every second, this is 0-120 rpm latestVal += (int)((new Random()).nextFloat() * 4); latestTime = SystemClock.elapsedRealtime()*1000000; } final long noDataNs = 5000 * 1000000L; if (!isSportEnabled || latestTime < 0 || latestVal == null || prevTime == latestTime && SystemClock.elapsedRealtime()*1000000 - latestTime < noDataNs ) { //No data in this point. Do not report 0 as the data just not is available yet but dont use last known as it may zero //report 0 after a grace time return null; } Float val; if (prevTime == latestTime || prevTime < 0 || prevVal == null) { //TODO Should the currentCadence be adjusted too? val = currentCadence; } else { val = 60 * (latestVal - prevVal) / 2 * 1000000000 / (latestTime - prevTime); } //"Consumed" values prevVal = latestVal; prevTime = latestTime; if (currentCadence == null) { currentCadence = val; } else { //Low pass filter final float alpha = 0.4f; currentCadence = val * alpha + (1 - alpha) * currentCadence; } return currentCadence; } @Override public void onSensorChanged(SensorEvent event) { if (event.values != null && event.values.length > 0) { if (!isStarted && (prevTime < 0 || event.timestamp - prevTime > 3000000000L)) { //one period to a few seconds before start so first getValue() after start/resume (may) have data prevTime = latestTime; prevVal = latestVal; } latestVal = event.values[0]; latestTime = event.timestamp; } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } /** * Sensor is available */ public static boolean isAvailable(final Context context) { return ((new TrackerCadence()).getSensor(context) != null) || testMode; } private Sensor getSensor(final Context context) { Sensor sensor = null; if (Build.VERSION.SDK_INT >= 20) { if (sensorManager == null) { sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); } //noinspection InlinedApi sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER); if (sensor == null) { sensorManager = null; } } if (testMode && sensor == null) { //No real sensor, emulate isEmulating = true; } return sensor; } /** * Called by Tracker during initialization */ @Override public ResultCode onInit(Callback callback, Context context) { return ResultCode.RESULT_OK; } @Override public ResultCode onConnecting(final Callback callback, final Context context) { ResultCode res; final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean enabled = prefs.getBoolean(context.getString(org.runnerup.R.string.pref_use_cadence_step_sensor), false); if (!enabled) { res = ResultCode.RESULT_NOT_ENABLED; } else { Sensor sensor = getSensor(context); if (sensor != null) { sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); res = ResultCode.RESULT_OK; } else if (isEmulating) { res = ResultCode.RESULT_OK; } else { res = ResultCode.RESULT_NOT_SUPPORTED; } } return res; } @Override public boolean isConnected() { return sensorManager != null; } @Override public void onConnected() { } /** * Called by Tracker before start * Component shall populate bindValues * with objects that will then be passed * to workout */ public void onBind(HashMap<String, Object> bindValues) { int sport = (int) bindValues.get(Constants.DB.ACTIVITY.SPORT); if (sport == Constants.DB.ACTIVITY.SPORT_BIKING) { //Not used, disconnect sensor so nothing is returned isSportEnabled = false; latestVal = null; } else { isSportEnabled = true; } } /** * Called by Tracker when workout starts */ @Override public void onStart() { isStarted = true; } /** * Called by Tracker when workout is paused */ @Override public void onPause() { isStarted = false; } /** * Called by Tracker when workout is resumed */ @Override public void onResume() { isStarted = true; } /** * Called by Tracker when workout is complete */ @Override public void onComplete(boolean discarded) { isStarted = false; } /** * Called by tracked after workout has ended */ @Override public ResultCode onEnd(Callback callback, Context context) { isStarted = false; if (sensorManager != null) { sensorManager.unregisterListener(this); } sensorManager = null; isEmulating = false; return ResultCode.RESULT_OK; } }