/*
* Copyright (C) 2012 - 2013 jonas.oreland@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;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.location.LocationListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
import org.runnerup.BuildConfig;
import org.runnerup.R;
import org.runnerup.common.tracker.TrackerState;
import org.runnerup.common.util.Constants;
import org.runnerup.common.util.ValueModel;
import org.runnerup.db.DBHelper;
import org.runnerup.export.SyncManager;
import org.runnerup.hr.HRProvider;
import org.runnerup.notification.ForegroundNotificationDisplayStrategy;
import org.runnerup.notification.NotificationState;
import org.runnerup.notification.NotificationStateManager;
import org.runnerup.notification.OngoingState;
import org.runnerup.tracker.component.TrackerComponent;
import org.runnerup.tracker.component.TrackerComponentCollection;
import org.runnerup.tracker.component.TrackerElevation;
import org.runnerup.tracker.component.TrackerGPS;
import org.runnerup.tracker.component.TrackerHRM;
import org.runnerup.tracker.component.TrackerPebble;
import org.runnerup.tracker.component.TrackerReceiver;
import org.runnerup.tracker.component.TrackerCadence;
import org.runnerup.tracker.component.TrackerTemperature;
import org.runnerup.tracker.component.TrackerPressure;
import org.runnerup.tracker.component.TrackerTTS;
import org.runnerup.tracker.component.TrackerWear;
import org.runnerup.tracker.filter.PersistentGpsLoggerListener;
import org.runnerup.util.Formatter;
import org.runnerup.util.HRZones;
import org.runnerup.workout.Scope;
import org.runnerup.workout.Workout;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* GpsTracker - this class tracks Location updates
*
* TODO: rename this class into ActivityTracker and factor out Gps stuff into own class
* that should be handled much like hrm (e.g as a sensor among others)
*
* @author jonas.oreland@gmail.com
*/
@TargetApi(Build.VERSION_CODES.FROYO)
public class Tracker extends android.app.Service implements
LocationListener, Constants {
public static final int MAX_HR_AGE = 3000; // 3s
private final Handler handler = new Handler();
TrackerComponentCollection components = new TrackerComponentCollection();
//Some trackers may select separate sensors depending on sport, handled in onBind()
TrackerGPS trackerGPS = (TrackerGPS) components.addComponent(new TrackerGPS(this));
TrackerHRM trackerHRM = (TrackerHRM) components.addComponent(new TrackerHRM());
TrackerTTS trackerTTS = (TrackerTTS) components.addComponent(new TrackerTTS());
private TrackerCadence trackerCadence = (TrackerCadence) components.addComponent(new TrackerCadence());
private TrackerTemperature trackerTemperature = (TrackerTemperature) components.addComponent(new TrackerTemperature());
private TrackerPressure trackerPressure = (TrackerPressure) components.addComponent(new TrackerPressure());
private TrackerElevation trackerElevation = (TrackerElevation) components.addComponent(new TrackerElevation(this, trackerGPS, trackerPressure));
TrackerReceiver trackerReceiver = (TrackerReceiver) components.addComponent(
new TrackerReceiver(this));
TrackerWear trackerWear; // created if version is sufficient
TrackerPebble trackerPebble; // created if version is sufficient
/**
* Work-around for http://code.google.com/p/android/issues/detail?id=23937
*/
boolean mBug23937Checked = false;
long mBug23937Delta = 0;
/**
*
*/
long mLapId = 0;
long mActivityId = 0;
long mElapsedTimeMillis = 0;
double mElapsedDistance = 0;
double mHeartbeats = 0;
double mHeartbeatMillis = 0; // since we might loose HRM connectivity...
long mMaxHR = 0;
final boolean mWithoutGps = false;
TrackerState nextState; //
final ValueModel<TrackerState> state = new ValueModel<TrackerState>(TrackerState.INIT);
int mLocationType = DB.LOCATION.TYPE_START;
/**
* Last location given by LocationManager
*/
Location mLastLocation = null;
//Second to last location - to get speed
Location mLast2Location = null;
/**
* Last location given by LocationManager when in state STARTED
*/
Location mActivityLastLocation = null;
SQLiteDatabase mDB = null;
PersistentGpsLoggerListener mDBWriter = null;
PowerManager.WakeLock mWakeLock = null;
final List<WorkoutObserver> liveLoggers = new ArrayList<WorkoutObserver>();
private Workout workout = null;
private NotificationStateManager notificationStateManager;
private NotificationState activityOngoingState;
@Override
public void onCreate() {
mDB =DBHelper.getWritableDatabase(this);
notificationStateManager = new NotificationStateManager(
new ForegroundNotificationDisplayStrategy(this));
wakeLock(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// >= 4.3
trackerWear = (TrackerWear) components.addComponent(new TrackerWear(this));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// >= 4.1
trackerPebble = (TrackerPebble) components.addComponent(new TrackerPebble(this));
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
if (mDB != null) {
DBHelper.closeDB(mDB);
mDB = null;
}
reset();
}
public void setup() {
switch (state.get()) {
case INIT:
break;
case INITIALIZING:
case INITIALIZED:
return;
case CONNECTING:
case CONNECTED:
case STARTED:
case PAUSED:
case ERROR:
case STOPPED:
if (BuildConfig.DEBUG) { throw new AssertionError(); }
return;
case CLEANUP:
/**
* if CLEANUP is in progress, setup will continue once complete
*/
nextState = TrackerState.INITIALIZING;
return;
}
state.set(TrackerState.INITIALIZING);
TrackerComponent.ResultCode result = components.onInit(onInitCallback,
getApplicationContext());
if (result != TrackerComponent.ResultCode.RESULT_PENDING) {
onInitCallback.run(components, result);
}
}
private final TrackerComponent.Callback onInitCallback = new TrackerComponent.Callback() {
@Override
public void run(TrackerComponent component, TrackerComponent.ResultCode resultCode) {
if (resultCode == TrackerComponent.ResultCode.RESULT_ERROR_FATAL) {
state.set(TrackerState.ERROR);
} else {
state.set(TrackerState.INITIALIZED);
}
Log.e(getClass().getName(), "state.set(" + getState() + ")");
handleNextState();
}
};
private void handleNextState() {
if (nextState == null)
return;
/* if last phase ended in error,
* don't continue with a new */
if (state.get() == TrackerState.ERROR)
return;
if (state.get() == nextState) {
nextState = null;
return;
}
switch(nextState) {
case INIT:
reset();
break;
case INITIALIZING:
break;
case INITIALIZED:
setup();
break;
case CONNECTING:
break;
case CONNECTED:
connect();
break;
case STARTED:
break;
case PAUSED:
break;
case STOPPED:
break;
case CLEANUP:
break;
case ERROR:
break;
}
}
public void connect() {
Log.e(getClass().getName(), "Tracker.connect() - state: " + state.get());
switch (state.get()) {
case INIT:
setup();
case INITIALIZING:
case CLEANUP:
nextState = TrackerState.CONNECTED;
Log.e(getClass().getName(), " => nextState: " + nextState);
return;
case INITIALIZED:
break;
case CONNECTING:
case CONNECTED:
return;
case STARTED:
case PAUSED:
case ERROR:
case STOPPED:
if (BuildConfig.DEBUG) { throw new AssertionError(); }
return;
}
state.set(TrackerState.CONNECTING);
wakeLock(true);
SyncManager u = new SyncManager(getApplicationContext());
u.loadLiveLoggers(liveLoggers);
u.close();
TrackerComponent.ResultCode result = components.onConnecting(onConnectCallback,
getApplicationContext());
if (result != TrackerComponent.ResultCode.RESULT_PENDING) {
onConnectCallback.run(components, result);
}
}
private final TrackerComponent.Callback onConnectCallback = new TrackerComponent.Callback() {
@Override
public void run(TrackerComponent component, TrackerComponent.ResultCode resultCode) {
if (resultCode == TrackerComponent.ResultCode.RESULT_ERROR_FATAL) {
state.set(TrackerState.ERROR);
} else if (state.get() == TrackerState.CONNECTING) {
state.set(TrackerState.CONNECTED);
/* now we're connected */
components.onConnected();
}
}
};
private long getBug23937Delta() {
return mBug23937Delta;
}
private long createActivity(int sport) {
Resources res = getResources();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean logGpxAccuracy = prefs.getBoolean(res.getString(R.string.pref_log_gpx_accuracy), false);
/**
* Create an Activity instance
*/
ContentValues tmp = new ContentValues();
tmp.put(DB.ACTIVITY.SPORT, sport);
mActivityId = mDB.insert(DB.ACTIVITY.TABLE, "nullColumnHack", tmp);
tmp.clear();
tmp.put(DB.LOCATION.ACTIVITY, mActivityId);
tmp.put(DB.LOCATION.LAP, 0); // always start with lap 0
mDBWriter = new PersistentGpsLoggerListener(mDB, DB.LOCATION.TABLE, tmp, logGpxAccuracy);
return mActivityId;
}
public void setWorkout(Workout workout) {
this.workout = workout;
}
public void start() {
// Log.e(getClass().getName(), "Tracker.start() state: " + state.get());
if (BuildConfig.DEBUG && state.get() != TrackerState.CONNECTED) { throw new AssertionError(); }
// connect workout and tracker
workout.setTracker(this);
/** Add Wear to live loggers if it's active */
if (components.getResultCode(TrackerWear.NAME) == TrackerComponent.ResultCode.RESULT_OK)
liveLoggers.add(trackerWear);
if (components.getResultCode(TrackerPebble.NAME) == TrackerComponent.ResultCode.RESULT_OK)
liveLoggers.add(trackerPebble);
/**
* create the DB activity
*/
createActivity(workout.getSport());
// do bindings
doBind();
// Let workout do initializations
workout.onInit(workout);
// Let components know we're starting
components.onStart();
mElapsedTimeMillis = 0;
mElapsedDistance = 0;
mHeartbeats = 0;
mHeartbeatMillis = 0;
mMaxHR = 0;
// TODO: check if mLastLocation is recent enough
mActivityLastLocation = null;
// New location update will be tagged with START
setNextLocationType(DB.LOCATION.TYPE_START);
state.set(TrackerState.STARTED);
activityOngoingState = new OngoingState(new Formatter(this), workout, this);
/**
* And finally let workout know that we started
*/
workout.onStart(Scope.ACTIVITY, this.workout);
}
private void doBind() {
/**
* Let components populate bindValues
*/
HashMap<String, Object> bindValues = new HashMap<String, Object>();
Context ctx = getApplicationContext();
bindValues.put(TrackerComponent.KEY_CONTEXT, ctx);
bindValues.put(Workout.KEY_FORMATTER, new Formatter(ctx));
bindValues.put(Workout.KEY_HRZONES, new HRZones(ctx));
bindValues.put(Workout.KEY_MUTE, workout.getMute());
bindValues.put(DB.ACTIVITY.SPORT, workout.getSport());
components.onBind(bindValues);
/**
* and then give them to workout
*/
workout.onBind(workout, bindValues);
}
public void newLap(ContentValues tmp) {
tmp.put(DB.LAP.ACTIVITY, mActivityId);
mLapId = mDB.insert(DB.LAP.TABLE, null, tmp);
ContentValues key = mDBWriter.getKey();
key.put(DB.LOCATION.LAP, tmp.getAsLong(DB.LAP.LAP));
mDBWriter.setKey(key);
}
public void saveLap(ContentValues tmp) {
tmp.put(DB.LAP.ACTIVITY, mActivityId);
String key[] = {
Long.toString(mLapId)
};
mDB.update(DB.LAP.TABLE, tmp, "_id = ?", key);
}
public void pause() {
switch (state.get()) {
case INIT:
case ERROR:
case INITIALIZING:
case INITIALIZED:
case PAUSED:
case CONNECTING:
case CONNECTED:
case CLEANUP:
case STOPPED:
return;
case STARTED:
break;
}
state.set(TrackerState.PAUSED);
setNextLocationType(DB.LOCATION.TYPE_PAUSE);
if (mActivityLastLocation != null) {
/**
* This saves mLastLocation as a PAUSE location
*/
internalOnLocationChanged(mActivityLastLocation);
}
saveActivity();
components.onPause();
}
public void stop() {
switch (state.get()) {
case INIT:
case ERROR:
case INITIALIZING:
case INITIALIZED:
case CONNECTING:
case CONNECTED:
case CLEANUP:
case STOPPED:
return;
case PAUSED:
case STARTED:
break;
}
state.set(TrackerState.STOPPED);
setNextLocationType(DB.LOCATION.TYPE_PAUSE);
if (mActivityLastLocation != null) {
/**
* This saves mLastLocation as a PAUSE location
*/
internalOnLocationChanged(mActivityLastLocation);
}
saveActivity();
components.onPause(); // TODO add new callback for this
}
private void internalOnLocationChanged(Location arg0) {
long save = mBug23937Delta;
mBug23937Delta = 0;
onLocationChangedImpl(arg0, true); // always save this location to db
mBug23937Delta = save;
}
public void resume() {
switch (state.get()) {
case INIT:
case ERROR:
case INITIALIZING:
case CLEANUP:
case INITIALIZED:
case CONNECTING:
case CONNECTED:
if (BuildConfig.DEBUG) { throw new AssertionError(); }
return;
case PAUSED:
case STOPPED:
break;
case STARTED:
return;
}
// TODO: check is mLastLocation is recent enough
mActivityLastLocation = mLastLocation;
state.set(TrackerState.STARTED);
setNextLocationType(DB.LOCATION.TYPE_RESUME);
if (mActivityLastLocation != null) {
/**
* save last know location as resume location
*/
internalOnLocationChanged(mActivityLastLocation);
}
}
public void reset() {
switch (state.get()) {
case INIT:
return;
case INITIALIZING:
// cleanup when INITIALIZE is complete
nextState = TrackerState.INIT;
return;
case INITIALIZED:
case ERROR:
case PAUSED:
case CONNECTING:
case CONNECTED:
case STOPPED:
nextState = TrackerState.INIT;
// it's ok to "abort" connecting
break;
case STARTED:
if (BuildConfig.DEBUG) { throw new AssertionError(); }
return;
case CLEANUP:
return;
}
wakeLock(false);
if (workout != null) {
workout.setTracker(null);
workout = null;
}
state.set(TrackerState.CLEANUP);
liveLoggers.clear();
TrackerComponent.ResultCode res = components.onEnd(onEndCallback, getApplicationContext());
if (res != TrackerComponent.ResultCode.RESULT_PENDING)
onEndCallback.run(components, res);
}
private final TrackerComponent.Callback onEndCallback = new TrackerComponent.Callback() {
@Override
public void run(TrackerComponent component, TrackerComponent.ResultCode resultCode) {
if (resultCode == TrackerComponent.ResultCode.RESULT_ERROR_FATAL) {
state.set(TrackerState.ERROR);
} else {
state.set(TrackerState.INIT);
}
handleNextState();
}
};
public void completeActivity(boolean save) {
if (BuildConfig.DEBUG &&
state.get() != TrackerState.PAUSED &&
state.get() != TrackerState.STOPPED) {
throw new AssertionError();
}
setNextLocationType(DB.LOCATION.TYPE_END);
if (mActivityLastLocation != null) {
internalOnLocationChanged(mActivityLastLocation);
}
if (save) {
saveActivity();
liveLog(DB.LOCATION.TYPE_END);
} else {
ContentValues tmp = new ContentValues();
tmp.put("deleted", 1);
String key[] = {
Long.toString(mActivityId)
};
mDB.update(DB.ACTIVITY.TABLE, tmp, "_id = ?", key);
liveLog(DB.LOCATION.TYPE_DISCARD);
}
components.onComplete(!save);
notificationStateManager.cancelNotification();
reset();
}
private void saveActivity() {
ContentValues tmp = new ContentValues();
if (mHeartbeatMillis > 0) {
long avgHR = Math.round((60 * 1000 * mHeartbeats) / mHeartbeatMillis); // BPM
tmp.put(Constants.DB.ACTIVITY.AVG_HR, avgHR);
}
if (mMaxHR > 0)
tmp.put(Constants.DB.ACTIVITY.MAX_HR, mMaxHR);
tmp.put(Constants.DB.ACTIVITY.DISTANCE, mElapsedDistance);
tmp.put(Constants.DB.ACTIVITY.TIME, getTime()); // time should be updated last for conditionalRecompute
if (TrackerPressure.isAvailable(this)) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean enabled = prefs.getBoolean(this.getString(org.runnerup.R.string.pref_use_pressure_sensor), false);
if (enabled) {
//Save information about barometer usage, used in uploads (like Strava)
tmp.put(DB.ACTIVITY.META_DATA, DB.ACTIVITY.WITH_BAROMETER);
}
}
String key[] = {
Long.toString(mActivityId)
};
mDB.update(DB.ACTIVITY.TABLE, tmp, "_id = ?", key);
}
void setNextLocationType(int newType) {
ContentValues key = mDBWriter.getKey();
key.put(DB.LOCATION.TYPE, newType);
mDBWriter.setKey(key);
mLocationType = newType;
}
public long getTime() {
return mElapsedTimeMillis / 1000;
}
public double getDistance() {
return mElapsedDistance;
}
public Location getLastKnownLocation() {
return mLastLocation;
}
public long getActivityId() {
return mActivityId;
}
@Override
public void onLocationChanged(Location arg0) {
//Elevation depends on GPS updates
trackerElevation.onLocationChanged(arg0);
onLocationChangedImpl(arg0, false);
}
private void onLocationChangedImpl(Location arg0, boolean internal) {
long now = System.currentTimeMillis();
if (!mBug23937Checked) {
long gpsTime = arg0.getTime();
if (gpsTime > now + 3 * 1000) {
mBug23937Delta = now - gpsTime;
} else {
mBug23937Delta = 0;
}
mBug23937Checked = true;
Log.e(getClass().getName(), "Bug23937: gpsTime: " + gpsTime + " utcTime: " + now
+ " (diff: " + Math.abs(gpsTime - now) + ") => delta: " + mBug23937Delta);
}
if (mBug23937Delta != 0) {
arg0.setTime(arg0.getTime() + mBug23937Delta);
}
if (internal || state.get() == TrackerState.STARTED) {
Integer hrValue = getCurrentHRValue(now, MAX_HR_AGE);
Double eleValue = getCurrentElevation();
Float cadValue = getCurrentCadence();
Float temperatureValue = getCurrentTemperature();
Float pressureValue = getCurrentPressure();
if (mActivityLastLocation != null) {
double timeDiff = (double) (arg0.getTime() - mActivityLastLocation
.getTime());
double distDiff = arg0.distanceTo(mActivityLastLocation);
if (timeDiff < 0) {
// time moved backward ??
Log.e(getClass().getName(), "lastTime: " + mActivityLastLocation.getTime());
Log.e(getClass().getName(), "arg0.getTime(): " + arg0.getTime());
Log.e(getClass().getName(), " => delta time: " + timeDiff);
Log.e(getClass().getName(), " => delta dist: " + distDiff);
// TODO investigate if this is known...only seems to happen
// in emulator
timeDiff = 0;
}
mElapsedTimeMillis += timeDiff;
mElapsedDistance += distDiff;
if (hrValue != null) {
mHeartbeats += (hrValue * timeDiff) / (60 * 1000);
mHeartbeatMillis += timeDiff; // TODO handle loss of HRM
// connection
mMaxHR = Math.max(hrValue, mMaxHR);
}
}
mActivityLastLocation = arg0;
mDBWriter.onLocationChanged(arg0, eleValue, mElapsedTimeMillis, mElapsedDistance, hrValue, cadValue, temperatureValue, pressureValue);
switch (mLocationType) {
case DB.LOCATION.TYPE_START:
case DB.LOCATION.TYPE_RESUME:
liveLog(mLocationType);
setNextLocationType(DB.LOCATION.TYPE_GPS);
break;
case DB.LOCATION.TYPE_GPS:
break;
case DB.LOCATION.TYPE_PAUSE:
break;
case DB.LOCATION.TYPE_END:
if (!internal && BuildConfig.DEBUG) { throw new AssertionError(); }
break;
}
liveLog(mLocationType);
notificationStateManager.displayNotificationState(activityOngoingState);
}
mLast2Location = mLastLocation;
mLastLocation = arg0;
}
private void liveLog(int type) {
for (WorkoutObserver l : liveLoggers) {
l.workoutEvent(workout, type);
}
}
@Override
public void onProviderDisabled(String arg0) {
}
@Override
public void onProviderEnabled(String arg0) {
}
@Override
public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
}
public TrackerState getState() {
return state.get();
}
public void registerTrackerStateListener(ValueModel.ChangeListener<TrackerState> listener) {
state.registerChangeListener(listener);
}
public void unregisterTrackerStateListener(ValueModel.ChangeListener<TrackerState> listener) {
state.unregisterChangeListener(listener);
}
/**
* Service interface stuff...
*/
public class LocalBinder extends android.os.Binder {
public Tracker getService() {
return Tracker.this;
}
}
private final IBinder mBinder = new LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private void wakeLock(boolean get) {
if (mWakeLock != null) {
if (mWakeLock.isHeld()) {
mWakeLock.release();
}
mWakeLock = null;
}
if (get) {
PowerManager pm = (PowerManager) this
.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"RunnerUp");
if (mWakeLock != null) {
mWakeLock.acquire();
}
}
}
public boolean isComponentConfigured(String name) {
switch (getState()) {
case INIT: // before onInit we don't know, so say no
case CLEANUP: // when cleaning, say no
case ERROR: // on error, say no
return false;
case INITIALIZING:
// If we're initializing...say no
if (components.getResultCode(name) == TrackerComponent.ResultCode.RESULT_PENDING)
return false;
case INITIALIZED:
case CONNECTING:
case CONNECTED:
case STARTED:
case PAUSED:
case STOPPED:
// check component
break;
}
switch (components.getResultCode(name)) {
case RESULT_OK:
case RESULT_PENDING:
return true;
case RESULT_NOT_SUPPORTED:
case RESULT_NOT_ENABLED:
case RESULT_ERROR:
case RESULT_ERROR_FATAL:
return false;
}
return false;
}
public boolean isComponentConnected(String name) {
TrackerComponent component = components.getComponent(name);
if (component == null)
return false;
return component.isConnected();
}
public HRProvider getHRProvider() {
return (trackerHRM.getHrProvider());
}
public Integer getCurrentHRValue(long now, long maxAge) {
HRProvider hrProvider = trackerHRM.getHrProvider();
if (hrProvider == null)
return null;
if (now > hrProvider.getHRValueTimestamp() + maxAge)
return null;
return hrProvider.getHRValue();
}
public Integer getCurrentHRValue() {
return getCurrentHRValue(System.currentTimeMillis(), 3000);
}
public Float getCurrentCadence() {
return trackerCadence.getValue();
}
public Float getCurrentTemperature() {
return trackerTemperature.getValue();
}
public Float getCurrentPressure() {
return trackerPressure.getValue();
}
public Double getCurrentElevation() {
return trackerElevation.getValue();
}
public Double getCurrentSpeed() {
return getCurrentSpeed(System.currentTimeMillis(), 3000);
}
private Double getCurrentSpeed(long now, long maxAge) {
if (mLastLocation == null)
return null;
if (now > mLastLocation.getTime() + maxAge)
return null;
double speed = mLastLocation.getSpeed();
if ((!mLastLocation.hasSpeed() || speed == 0.0f)
&& mLastLocation != null && mLast2Location != null &&
mLastLocation.getTime() > mLast2Location.getTime() ) {
//Some Android (at least emulators) do not implement getSpeed() (even if hasSpeed() is true)
speed = mLastLocation.distanceTo(mLast2Location) * 1000 / (mLastLocation.getTime() - mLast2Location.getTime());
}
return speed;
}
public double getHeartbeats() {
return mHeartbeats;
}
public Integer getCurrentBatteryLevel() {
HRProvider hrProvider = trackerHRM.getHrProvider();
if (hrProvider == null)
return null;
return hrProvider.getBatteryLevel();
}
public Workout getWorkout() {
return workout;
}
}