/* * 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.workout; import android.annotation.TargetApi; import android.content.ContentValues; import android.content.SharedPreferences; import android.location.Location; import android.os.Build; import org.runnerup.BuildConfig; import org.runnerup.common.util.Constants.DB; import org.runnerup.tracker.Tracker; import org.runnerup.tracker.component.TrackerHRM; import org.runnerup.tracker.component.TrackerCadence; import org.runnerup.tracker.component.TrackerTemperature; import org.runnerup.tracker.component.TrackerPressure; import org.runnerup.util.HRZones; import org.runnerup.workout.feedback.RUTextToSpeech; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; /** * This class is the top level object for a workout, it is being called by * RunActivity, and by the Workout components */ @TargetApi(Build.VERSION_CODES.FROYO) public class Workout implements WorkoutComponent, WorkoutInfo { long lap = 0; int currentStepNo = -1; Step currentStep = null; boolean paused = false; final ArrayList<Step> steps = new ArrayList<Step>(); final ArrayList<WorkoutStepListener> stepListeners = new ArrayList<WorkoutStepListener>(); int sport = DB.ACTIVITY.SPORT_RUNNING; private boolean mute; class PendingFeedback { int depth = 0; final HashSet<Feedback> set = new HashSet<Feedback>(); // For uniquing void init() { depth++; } void add(Feedback f) { if (set.contains(f)) return; set.add(f); try { f.emit(Workout.this, tracker.getApplicationContext()); } catch (Exception ex) { // make sure that no small misstake crashes a workout... ex.printStackTrace(); } } boolean end() { --depth; if (depth == 0) { set.clear(); try { Workout.this.textToSpeech.emit(); } catch (Exception ex) { // make sure that no small misstake crashes a workout... ex.printStackTrace(); } return true; } return false; } } final PendingFeedback pendingFeedback = new PendingFeedback(); Tracker tracker = null; SharedPreferences audioCuePrefs; HRZones hrZones = null; RUTextToSpeech textToSpeech = null; public static final String KEY_TTS = "tts"; public static final String KEY_COUNTER_VIEW = "CountdownView"; public static final String KEY_FORMATTER = "Formatter"; public static final String KEY_HRZONES = "HrZones"; public static final String KEY_MUTE = "mute"; public Workout() { } public void setTracker(Tracker tracker) { this.tracker = tracker; } public void onInit(Workout w) { if (BuildConfig.DEBUG && w != this) { throw new AssertionError(); } for (Step a : steps) { a.onInit(this); } } public void onBind(Workout w, HashMap<String, Object> bindValues) { if (bindValues.containsKey(Workout.KEY_HRZONES)) hrZones = (HRZones) bindValues.get(Workout.KEY_HRZONES); if (bindValues.containsKey(Workout.KEY_TTS)) textToSpeech = (RUTextToSpeech) bindValues.get(Workout.KEY_TTS); for (Step a : steps) { a.onBind(w, bindValues); } } public void onEnd(Workout w) { if (BuildConfig.DEBUG && w != this) { throw new AssertionError(); } for (Step a : steps) { a.onEnd(this); } } @Override public void onRepeat(int current, int limit) { } public void onStart(Scope s, Workout w) { if (BuildConfig.DEBUG && w != this) { throw new AssertionError(); } initFeedback(); for (Step st : steps) { st.onRepeat(0, 1); } currentStepNo = 0; if (steps.size() > 0) { setCurrentStep(steps.get(currentStepNo)); } if (currentStep != null) { currentStep.onStart(Scope.ACTIVITY, this); currentStep.onStart(Scope.STEP, this); currentStep.onStart(Scope.LAP, this); } emitFeedback(); } private void setCurrentStep(Step step) { Step oldStep = currentStep; currentStep = step; Step newStep = (step == null) ? null : step.getCurrentStep(); for (WorkoutStepListener l : stepListeners) { l.onStepChanged(oldStep, newStep); } } public void onTick() { initFeedback(); while (currentStep != null) { boolean finished = currentStep.onTick(this); if (finished == false) break; onNextStep(); } emitFeedback(); } public void onNextStep() { currentStep.onComplete(Scope.LAP, this); currentStep.onComplete(Scope.STEP, this); if (currentStep.onNextStep(this)) currentStepNo++; if (currentStepNo < steps.size()) { setCurrentStep(steps.get(currentStepNo)); currentStep.onStart(Scope.STEP, this); currentStep.onStart(Scope.LAP, this); } else { currentStep.onComplete(Scope.ACTIVITY, this); setCurrentStep(null); tracker.stop(); } } public void onPause(Workout w) { initFeedback(); if (currentStep != null) { currentStep.onPause(this); } emitFeedback(); paused = true; } public void onNewLap() { initFeedback(); if (currentStep != null) { currentStep.onComplete(Scope.LAP, this); currentStep.onStart(Scope.LAP, this); } emitFeedback(); } public void onNewLapOrNextStep() { if (!isLastStep()) { onNextStep(); } else { onNewLap(); } } public void onStop(Workout w) { initFeedback(); if (currentStep != null) { currentStep.onStop(this); } emitFeedback(); } public void onResume(Workout w) { initFeedback(); if (currentStep != null) { currentStep.onResume(this); } emitFeedback(); paused = false; } public void onComplete(Scope s, Workout w) { if (currentStep != null) { currentStep.onComplete(Scope.LAP, this); currentStep.onComplete(Scope.STEP, this); currentStep.onComplete(Scope.ACTIVITY, this); } setCurrentStep(null); currentStepNo = -1; } public void onSave() { tracker.completeActivity(true); } public void onDiscard() { tracker.completeActivity(false); } @Override public boolean isPaused() { return paused; } @Override public double get(Scope scope, Dimension d) { switch (d) { case DISTANCE: return getDistance(scope); case TIME: return getTime(scope); case SPEED: return getSpeed(scope); case PACE: return getPace(scope); case HR: return getHeartRate(scope); case HRZ: return getHeartRateZone(scope); case CAD: return getCadence(scope); case TEMPERATURE: return getTemperature(scope); case PRESSURE: return getPressure(scope); } return 0; } @Override public double getDistance(Scope scope) { switch (scope) { case ACTIVITY: return tracker.getDistance(); case STEP: case LAP: if (currentStep != null) return currentStep.getDistance(this, scope); if (BuildConfig.DEBUG) { throw new AssertionError(); } break; case CURRENT: break; } return 0; } @Override public double getTime(Scope scope) { switch (scope) { case ACTIVITY: return tracker.getTime(); case STEP: case LAP: if (currentStep != null) return currentStep.getTime(this, scope); if (BuildConfig.DEBUG) { throw new AssertionError(); } break; case CURRENT: return System.currentTimeMillis() / 1000; // now } return 0; } @Override public double getSpeed(Scope scope) { switch (scope) { case ACTIVITY: double d = getDistance(scope); double t = getTime(scope); if (t == 0) return (double) 0; return d / t; case STEP: case LAP: if (currentStep != null) return currentStep.getSpeed(this, scope); break; case CURRENT: Double s = tracker.getCurrentSpeed(); if (s != null) return s; return 0; } return 0; } @Override public double getPace(Scope scope) { double s = getSpeed(scope); if (s != 0) return 1.0d / s; return 0; } @Override public double getDuration(Scope scope, Dimension dimension) { if (scope == Scope.STEP && currentStep != null) { return currentStep.getDuration(dimension); } return 0; } @Override public double getRemaining(Scope scope, Dimension dimension) { double curr = this.get(scope, dimension); double duration = this.getDuration(scope, dimension); if (duration > curr) { return duration - curr; } else { return 0; } } double getHeartbeats(Scope scope) { switch (scope) { case ACTIVITY: return tracker.getHeartbeats(); case STEP: case LAP: if (currentStep != null) return currentStep.getHeartbeats(this, scope); return 0; case CURRENT: return 0; } return 0; } @Override public double getHeartRate(Scope scope) { switch (scope) { case CURRENT: { Integer val = tracker.getCurrentHRValue(); if (val == null) return 0; return val; } case LAP: case STEP: case ACTIVITY: break; } double t = getTime(scope); // in seconds double b = getHeartbeats(scope); // total (estimated) beats during // workout if (t != 0) { return (60 * b) / t; // bpm } return 0.0; } @Override public double getCadence(Scope scope) { switch (scope) { case CURRENT: { Float val = tracker.getCurrentCadence(); if (val == null) return -1; //TODO should not be used return val; } case LAP: case STEP: case ACTIVITY: break; } double t = getTime(scope); // in seconds double b = -1; //TODO get steps for scope if (BuildConfig.DEBUG) { throw new AssertionError(); } if (t != 0) { return (60 * b)/ 2 / t; // bpm } return 0.0; } @Override public double getTemperature(Scope scope) { switch (scope) { case CURRENT: { Float val = tracker.getCurrentTemperature(); if (val == null) return -1; //TODO should not be used return val; } case LAP: case STEP: case ACTIVITY: break; } //TODO if (BuildConfig.DEBUG) { throw new AssertionError(); } return 0.0; } @Override public double getPressure(Scope scope) { switch (scope) { case CURRENT: { Float val = tracker.getCurrentPressure(); if (val == null) return -1; //TODO should not be used return val; } case LAP: case STEP: case ACTIVITY: break; } //TODO if (BuildConfig.DEBUG) { throw new AssertionError(); } return 0.0; } @Override public double getHeartRateZone(Scope scope) { return hrZones.getZone(getHeartRate(scope)); } @Override public int getSport() { return sport; } @Override public boolean isEnabled(Dimension dim, Scope scope) { if (dim == Dimension.HR) { return tracker.isComponentConnected(TrackerHRM.NAME); } else if (dim == Dimension.HRZ) { if (hrZones == null || !hrZones.isConfigured() || !tracker.isComponentConnected(TrackerHRM.NAME)) return false; } else if (dim == Dimension.CAD) { return tracker.isComponentConnected(TrackerCadence.NAME); } else if (dim == Dimension.TEMPERATURE) { return tracker.isComponentConnected(TrackerTemperature.NAME); } else if (dim == Dimension.PRESSURE) { return tracker.isComponentConnected(TrackerPressure.NAME); } else if ((dim == Dimension.SPEED || dim == Dimension.PACE) && scope == Scope.CURRENT) { return tracker.getCurrentSpeed() != null; } return true; } private void initFeedback() { pendingFeedback.init(); } public void addFeedback(Feedback f) { pendingFeedback.add(f); } private void emitFeedback() { pendingFeedback.end(); } void newLap(ContentValues tmp) { tmp.put(DB.LAP.LAP, lap); tracker.newLap(tmp); } void saveLap(ContentValues tmp, boolean next) { tracker.saveLap(tmp); if (next) { lap++; } } public int getStepCount() { return steps.size(); } public boolean isLastStep() { if (currentStepNo + 1 < steps.size()) return false; if (currentStepNo < steps.size()) return steps.get(currentStepNo).isLastStep(); return true; } /** * @return flattened list of all steps in workout */ static public class StepListEntry { public StepListEntry(int index, Step step, int level, Step parent) { this.index = index; this.level = level; this.step = step; this.parent = parent; } public final int index; public final int level; public final Step parent; public final Step step; } public void addStep(Step s) { steps.add(s); } public List<Step> getSteps() { return steps; } public List<StepListEntry> getStepList() { ArrayList<StepListEntry> list = new ArrayList<StepListEntry>(); for (Step s : steps) { s.getSteps(null, 0, list); } return list; } public Step getCurrentStep() { if (currentStepNo >= 0 && currentStepNo < steps.size()) return steps.get(currentStepNo).getCurrentStep(); return null; } public void registerWorkoutStepListener(WorkoutStepListener listener) { stepListeners.add(listener); } public void unregisterWorkoutStepListener(WorkoutStepListener listener) { stepListeners.remove(listener); } private static class FakeWorkout extends Workout { FakeWorkout() { super(); } @Override public boolean isEnabled(Dimension dim, Scope scope) { return true; } public double getDistance(Scope scope) { switch (scope) { case ACTIVITY: return (3000 + 7000 * Math.random()); case STEP: return (300 + 700 * Math.random()); case LAP: return (300 + 700 * Math.random()); case CURRENT: return 0; } return 0; } public double getTime(Scope scope) { switch (scope) { case ACTIVITY: return (10 * 60 + 50 * 60 * Math.random()); case STEP: return (1 * 60 + 5 * 60 * Math.random()); case LAP: return (1 * 60 + 5 * 60 * Math.random()); case CURRENT: return System.currentTimeMillis() / 1000; } return 0; } public double getSpeed(Scope scope) { double d = getDistance(scope); double t = getTime(scope); if (t == 0) return 0; return d / t; } public double getHeartRate(Scope scope) { return 150 + 25 * Math.random(); } } @Override public Location getLastKnownLocation() { return tracker.getLastKnownLocation(); } public static Workout fakeWorkoutForTestingAudioCue() { FakeWorkout w = new FakeWorkout(); return w; } public void setMute(boolean mute) { this.mute = mute; } public boolean getMute() { return mute; } }