Java tutorial
/* * Copyright (C) 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.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import android.util.Pair; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.runnerup.R; import org.runnerup.export.util.SyncHelper; import org.runnerup.util.SafeParse; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Stack; @TargetApi(Build.VERSION_CODES.FROYO) public class WorkoutSerializer { public static final String WORKOUTS_DIR = "workouts"; private static String getString(JSONObject obj, String key) { try { return obj.getString(key); } catch (JSONException e) { } return null; } private static Integer getInt(JSONObject obj, String key) { try { return obj.getInt(key); } catch (JSONException e) { } return null; } private static class jsonstep { int order; Integer group; Integer parentGroup; RepeatStep parentStep; Step step; } public static Workout readJSON(Reader in, boolean convertRestToRecovery) throws JSONException { JSONObject obj = SyncHelper.parse(in); obj = obj.getJSONObject("com.garmin.connect.workout.json.UserWorkoutJson"); Workout w = new Workout(); JSONArray steps = obj.getJSONArray("workoutSteps"); int stepNo = 0; JSONObject step = null; ArrayList<jsonstep> list = new ArrayList<jsonstep>(4); while ((step = steps.optJSONObject(stepNo)) != null) { jsonstep js = parseStep(step, convertRestToRecovery); list.add(js); stepNo++; } for (jsonstep s : list) { if (s.parentGroup != null) { s.parentStep = findRepeatStep(list, s.parentGroup); } else { } } Collections.sort(list, new Comparator<jsonstep>() { @Override public int compare(jsonstep lhs, jsonstep rhs) { return lhs.order - rhs.order; } }); for (jsonstep s : list) { if (s.parentStep != null) { s.parentStep.steps.add(s.step); } else { w.steps.add(s.step); } } return w; } private static RepeatStep findRepeatStep(ArrayList<jsonstep> list, int groupId) { for (jsonstep s : list) { if (s.group != null && s.group == groupId && s.step instanceof RepeatStep) { return (RepeatStep) s.step; } } return null; } private static Intensity getIntensity(JSONObject obj) throws JSONException { String stepTypeKey = obj.getString("stepTypeKey"); if (stepTypeKey.equalsIgnoreCase("warmup")) return Intensity.WARMUP; else if (stepTypeKey.equalsIgnoreCase("repeat")) return Intensity.REPEAT; else if (stepTypeKey.equalsIgnoreCase("rest")) return Intensity.RESTING; else if (stepTypeKey.equalsIgnoreCase("recovery")) return Intensity.RECOVERY; else if (stepTypeKey.equalsIgnoreCase("cooldown")) return Intensity.COOLDOWN; // @TODO look at intensityTypeKey too?? else if (stepTypeKey.equalsIgnoreCase("interval")) return Intensity.ACTIVE; else if (stepTypeKey.equalsIgnoreCase("other")) return Intensity.ACTIVE; return null; } private static void putIntensity(JSONObject obj, Intensity intensity) throws JSONException { String val = null; switch (intensity) { case ACTIVE: val = "interval"; break; case RESTING: val = "rest"; break; case WARMUP: val = "warmup"; break; case COOLDOWN: val = "cooldown"; break; case REPEAT: val = "repeat"; break; case RECOVERY: val = "recovery"; break; } if (val != null) { obj.put("stepTypeKey", val); } } private static final Pair<Dimension, Double> NullDimensionPair = new Pair<Dimension, Double>(null, 0.0); private static Pair<Dimension, Double> getDuration(JSONObject obj, Intensity intensity) throws JSONException { Dimension dim = null; double val = 0; String endConditionTypeKey = obj.getString("endConditionTypeKey"); if (endConditionTypeKey.equalsIgnoreCase("lap.button")) { return NullDimensionPair; } else if (endConditionTypeKey.equalsIgnoreCase("iterations")) { val = SafeParse.parseDouble(getString(obj, "endConditionValue"), 1); } else if (endConditionTypeKey.equalsIgnoreCase("distance")) { dim = Dimension.DISTANCE; val = SafeParse.parseDouble(getString(obj, "endConditionValue"), 0); val = scale(val, obj, "endConditionUnitKey"); } else if (endConditionTypeKey.equalsIgnoreCase("time")) { dim = Dimension.TIME; val = SafeParse.parseDouble(getString(obj, "endConditionValue"), 0); val = scale(val, obj, "endConditionUnitKey"); } else if (endConditionTypeKey.equalsIgnoreCase("calories")) { // not implemented return NullDimensionPair; } else if (endConditionTypeKey.equalsIgnoreCase("heart.rate")) { // not implemented return NullDimensionPair; } return new Pair<Dimension, Double>(dim, val); } private static void putDuration(JSONObject obj, Step step, Dimension durationType, double durationValue) throws JSONException { if (step.getIntensity() == Intensity.REPEAT) { obj.put("endConditionTypeKey", "iterations"); obj.put("endConditionValue", ((RepeatStep) step).getRepeatCount()); obj.put("endConditionUnitKey", "dimensionless"); return; } if (durationType == null) { obj.put("endConditionTypeKey", "lap.button"); obj.put("endConditionUnitKey", "dimensionless"); return; } switch (durationType) { case TIME: obj.put("endConditionTypeKey", "time"); obj.put("endConditionValue", 1000 * durationValue); obj.put("endConditionUnitKey", "ms"); break; case DISTANCE: obj.put("endConditionTypeKey", "distance"); obj.put("endConditionValue", 100 * durationValue); obj.put("endConditionUnitKey", "centimeter"); break; case SPEED: break; case PACE: break; case HR: break; case HRZ: break; } } private static double scale(double val, JSONObject obj, String key) { String unit = getString(obj, key); if (unit == null) return val; if (unit.equalsIgnoreCase("dimensionless")) return val; /** * The below are one I found in the json objects... */ if (unit.equalsIgnoreCase("centimeter")) return val / 100.0; if (unit.equalsIgnoreCase("ms")) return val / 1000.0; if (unit.equalsIgnoreCase("kilojoule")) return val; if (unit.equalsIgnoreCase("bpm")) return val; /** * The below I just added for "completeness" */ if (unit.equalsIgnoreCase("millimeter")) return val / 1000.0; if (unit.equalsIgnoreCase("kilometer")) return val * 1000.0; if (unit.equalsIgnoreCase("miles")) return val * 1609.34; return val; } private static final Pair<Dimension, Range> NullTargetPair = new Pair<Dimension, Range>(null, null); private static Pair<Dimension, Range> getTarget(JSONObject obj) { String targetTypeKey = getString(obj, "targetTypeKey"); if (targetTypeKey == null) return NullTargetPair; if (targetTypeKey.equalsIgnoreCase("no.target")) return NullTargetPair; Dimension dim = null; Range range = null; if (targetTypeKey.equalsIgnoreCase("pace.zone")) { dim = Dimension.PACE; range = new Range(SafeParse.parseDouble(getString(obj, "targetValueOne"), 0), SafeParse.parseDouble(getString(obj, "targetValueTwo"), 0)); scale(range, dim, obj, "targetValueUnitKey"); } else if (targetTypeKey.equalsIgnoreCase("speed.zone")) { dim = Dimension.SPEED; range = new Range(SafeParse.parseDouble(getString(obj, "targetValueOne"), 0), SafeParse.parseDouble(getString(obj, "targetValueTwo"), 0)); scale(range, dim, obj, "targetValueUnitKey"); } else if (targetTypeKey.equalsIgnoreCase("heart.rate.zone")) { dim = Dimension.HR; range = new Range(SafeParse.parseDouble(getString(obj, "targetValueOne"), 0), SafeParse.parseDouble(getString(obj, "targetValueTwo"), 0)); scale(range, dim, obj, "targetValueUnitKey"); } else if (targetTypeKey.equalsIgnoreCase("cadence")) { // Not implemented return NullTargetPair; } return new Pair<Dimension, Range>(dim, range); } private static void putTarget(JSONObject obj, Step step, Dimension targetType, Range targetValue) throws JSONException { if (step.getIntensity() == Intensity.REPEAT) return; if (targetType == null) { obj.put("targetTypeKey", "no.target"); obj.put("targetValueUnitKey", "dimensionless"); return; } final double centimeterPerMilliseconds = 100.0 / 1000.0; switch (targetType) { case TIME: break; case DISTANCE: break; case SPEED: obj.put("targetTypeKey", "speed.zone"); obj.put("targetValueOne", centimeterPerMilliseconds * targetValue.minValue); obj.put("targetValueTwo", centimeterPerMilliseconds * targetValue.maxValue); obj.put("targetValueUnitKey", "centimetersPerMillisecond"); break; case PACE: obj.put("targetTypeKey", "pace.zone"); obj.put("targetValueOne", centimeterPerMilliseconds * (targetValue.maxValue != 0 ? 1.0 / targetValue.maxValue : 0)); obj.put("targetValueTwo", centimeterPerMilliseconds * (targetValue.minValue != 0 ? 1.0 / targetValue.minValue : 0)); obj.put("targetValueUnitKey", "centimetersPerMillisecond"); break; case HR: obj.put("targetTypeKey", "heart.rate.zone"); obj.put("targetValueOne", targetValue.minValue); obj.put("targetValueTwo", targetValue.maxValue); obj.put("targetValueUnitKey", "bpm"); break; case HRZ: break; } } private static void scale(Range range, Dimension dim, JSONObject obj, String key) { String unit = getString(obj, key); if (unit == null) return; if (unit.equalsIgnoreCase("dimensionless")) return; double factor = 1.0; Dimension unitDim = dim; /** * The below are one I found in the json objects... */ if (unit.equalsIgnoreCase("centimetersPerMillisecond")) { factor = 1000.0 / 100.0; unitDim = Dimension.SPEED; } else if (unit.equalsIgnoreCase("stepsPerMinute")) { // not implemented } else if (unit.equalsIgnoreCase("bpm")) { // not implemented } /** * The below I just added for "completeness" */ else if (unit.equalsIgnoreCase("metersPerMillisecond")) { factor = 1000.0 / 1.0; unitDim = Dimension.SPEED; } else if (unit.equalsIgnoreCase("metersPerSecond")) { factor = 1.0 / 1.0; unitDim = Dimension.SPEED; } else if (unit.equalsIgnoreCase("centimetersPerSecond")) { factor = 1.0 / 100.0; unitDim = Dimension.SPEED; } if (unit.equalsIgnoreCase("millisecondsPerCentimeter")) { factor = 100.0 / 1000.0; unitDim = Dimension.PACE; } else if (unit.equalsIgnoreCase("millisecondsPerMeter")) { factor = 1.0 / 1000.0; unitDim = Dimension.PACE; } else if (unit.equalsIgnoreCase("secondsPerMeter")) { factor = 1.0 / 1.0; unitDim = Dimension.PACE; } else if (unit.equalsIgnoreCase("secondsPerCentimeters")) { factor = 100.0 / 1.0; unitDim = Dimension.PACE; } range.minValue *= factor; range.maxValue *= factor; if (dim == Dimension.SPEED && unitDim == Dimension.PACE || dim == Dimension.PACE && unitDim == Dimension.SPEED) { range.minValue = range.minValue == 0 ? 0 : 1.0 / range.minValue; range.maxValue = range.maxValue == 0 ? 0 : 1.0 / range.maxValue; if (range.minValue > range.maxValue) { double tmp = range.minValue; range.minValue = range.maxValue; range.maxValue = tmp; } } } private static jsonstep parseStep(JSONObject obj, boolean convertRestToRecovery) throws JSONException { jsonstep js = new jsonstep(); js.order = obj.getInt("stepOrder"); js.group = getInt(obj, "groupId"); js.parentGroup = getInt(obj, "parentGroupId"); Intensity intensity = getIntensity(obj); Pair<Dimension, Double> duration = getDuration(obj, intensity); Pair<Dimension, Range> target = getTarget(obj); switch (intensity) { case REPEAT: { RepeatStep rs = new RepeatStep(); rs.repeatCount = duration.second.intValue(); js.step = rs; break; } case RESTING: if (convertRestToRecovery && duration.first == Dimension.DISTANCE && duration.second != null) { // fall through } else { js.step = Step.createPauseStep(duration.first, duration.second); break; } case ACTIVE: case WARMUP: case COOLDOWN: case RECOVERY: js.step = new Step(); js.step.intensity = intensity; js.step.durationType = duration.first; js.step.durationValue = duration.second; js.step.targetType = target.first; js.step.targetValue = target.second; break; } return js; } public static File getFile(Context ctx, String name) { return new File(ctx.getDir(WORKOUTS_DIR, 0).getPath() + File.separator + name); } public static Workout readFile(Context ctx, String name) throws FileNotFoundException, JSONException { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); File fin = getFile(ctx, name); Log.e("WorkoutSerializer", "reading " + fin.getPath()); final boolean convertRestToRecovery = prefs.getBoolean( ctx.getResources().getString(R.string.pref_convert_advanced_distance_rest_to_recovery), false); return readJSON(new FileReader(fin), convertRestToRecovery); } public static void writeFile(Context ctx, String name, Workout workout) throws IOException, JSONException { File fout = getFile(ctx, name); Log.e("WorkoutSerializer", "writing " + fout.getPath()); writeJSON(new FileWriter(fout), workout); } public static void writeJSON(Writer out, Workout workout) throws JSONException, IOException { JSONObject obj = createJSON(workout); out.write(obj.toString()); out.flush(); } public static JSONObject createJSON(Workout workout) throws JSONException { Stack<jsonstep> stepStack = new Stack<jsonstep>(); ArrayList<jsonstep> stepList = new ArrayList<jsonstep>(); int no = 1; int group = 1; Workout.StepListEntry prev = null; for (Workout.StepListEntry e : workout.getStepList()) { jsonstep s = new jsonstep(); s.step = e.step; s.order = no++; if (e.parent != null) { while (e.parent != stepStack.peek().step) { stepStack.pop(); group = stepStack.peek().group; } s.parentGroup = stepStack.peek().group; s.parentStep = (RepeatStep) stepStack.peek().step; } if (e.step instanceof RepeatStep) { group++; stepStack.push(s); } if (e.parent == null && prev != null && prev.parent != null) { group++; } s.group = group; stepList.add(s); prev = e; } JSONArray steps = new JSONArray(); for (jsonstep s : stepList) { JSONObject obj = toJSON(s.step); obj.put("stepOrder", s.order); obj.put("groupId", s.group); if (s.parentGroup != null) { obj.put("parentGroupId", s.parentGroup.intValue()); } steps.put(obj); } JSONObject obj = new JSONObject(); obj.put("workoutSteps", steps); JSONObject ret = new JSONObject(); ret.put("com.garmin.connect.workout.json.UserWorkoutJson", obj); return ret; } private static JSONObject toJSON(Step step) throws JSONException { JSONObject obj = new JSONObject(); putIntensity(obj, step.getIntensity()); putDuration(obj, step, step.getDurationType(), step.getDurationValue()); putTarget(obj, step, step.getTargetType(), step.getTargetValue()); return obj; } }