/* * Copyright (C) 2014 paradix@10g.pl * * 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.export.format; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.util.Log; import android.util.Pair; import org.runnerup.R; import org.runnerup.export.GoogleFitSynchronizer; import org.runnerup.export.util.SyncHelper; import org.runnerup.util.JsonWriter; import org.runnerup.workout.Sport; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.runnerup.common.util.Constants.DB; @TargetApi(Build.VERSION_CODES.FROYO) public class GoogleFitData { public static final int SECONDS_TO_MILLIS = 1000; public static final int MICRO_TO_NANOS = 1000000; public static final int SECONDS_TO_NANOS = 1000000000; private static final Map<Sport, Integer> ACTIVITY_TYPE; static { Map<Sport, Integer> aMap = new HashMap<>(); // sports list can be found at https://developers.google.com/fit/rest/v1/reference/activity-types aMap.put(Sport.RUNNING, 8); aMap.put(Sport.BIKING, 1); aMap.put(Sport.OTHER, 4); aMap.put(Sport.ORIENTEERING, 4); //not supported so considering unknown aMap.put(Sport.WALKING, 7); ACTIVITY_TYPE = Collections.unmodifiableMap(aMap); } private static final Map<DataSourceType, List<DataTypeField>> DATA_TYPE_FIELDS; static { Map<DataSourceType, List<DataTypeField>> fieldsMap = new HashMap<DataSourceType, List<DataTypeField>>(); List<DataTypeField> fields = new ArrayList<DataTypeField>(); Pair<String, String> floatPoint = Pair.create("floatPoint", "fpVal"); Pair<String, String> integer = Pair.create("integer", "intVal"); fields.add(new DataTypeField("activity", integer, DB.ACTIVITY.SPORT)); fieldsMap.put(DataSourceType.ACTIVITY_SEGMENT, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("bpm", floatPoint, DB.LOCATION.HR)); fieldsMap.put(DataSourceType.ACTIVITY_HEARTRATE, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("latitude", floatPoint, DB.LOCATION.LATITUDE)); fields.add(new DataTypeField("longitude", floatPoint, DB.LOCATION.LONGITUDE)); fields.add(new DataTypeField("accuracy", floatPoint, DB.LOCATION.ACCURANCY)); fields.add(new DataTypeField("altitude", floatPoint, DB.LOCATION.ALTITUDE)); fieldsMap.put(DataSourceType.ACTIVITY_LOCATION, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("speed", floatPoint, DB.LOCATION.SPEED)); fieldsMap.put(DataSourceType.ACTIVITY_SPEED, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("activity", integer, DB.ACTIVITY.SPORT)); fields.add(new DataTypeField("duration", integer, DB.ACTIVITY.TIME)); fields.add(new DataTypeField("num_segments", integer, "1")); fieldsMap.put(DataSourceType.ACTIVITY_SUMMARY, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("average", floatPoint, "AVG(" + DB.LOCATION.HR + ")")); fields.add(new DataTypeField("max", floatPoint, "MAX(" + DB.LOCATION.HR + ")")); fields.add(new DataTypeField("min", floatPoint, "MIN(" + DB.LOCATION.HR + ")")); fieldsMap.put(DataSourceType.HEARTRATE_SUMMARY, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("low_latitude", floatPoint, "MIN(" + DB.LOCATION.LATITUDE + ")")); fields.add(new DataTypeField("high_latitude", floatPoint, "MAX(" + DB.LOCATION.LATITUDE + ")")); fields.add(new DataTypeField("low_longitude", floatPoint, "MIN(" + DB.LOCATION.LONGITUDE + ")")); fields.add(new DataTypeField("high_longitude", floatPoint, "MAX(" + DB.LOCATION.LONGITUDE + ")")); fieldsMap.put(DataSourceType.LOCATION_SUMMARY, fields); fields = new ArrayList<DataTypeField>(); fields.add(new DataTypeField("average", floatPoint, "AVG(" + DB.LOCATION.SPEED + ")")); fields.add(new DataTypeField("max", floatPoint, "MAX(" + DB.LOCATION.SPEED + ")")); fields.add(new DataTypeField("min", floatPoint, "MIN(" + DB.LOCATION.SPEED + ")")); fieldsMap.put(DataSourceType.SPEED_SUMMARY, fields); DATA_TYPE_FIELDS = Collections.unmodifiableMap(fieldsMap); } private final Context mContext; private final String mProjectId; private SQLiteDatabase mDB = null; public GoogleFitData(final SQLiteDatabase db, String project, Context ctx) { this.mDB = db; this.mProjectId = project; this.mContext = ctx; } public final String getProjectId() { return mProjectId; } public final SQLiteDatabase getDB() { return mDB; } public final List<DataSourceType> getActivityDataSourceTypes(long activityId) { List<DataSourceType> neededSources = new ArrayList<DataSourceType>(); String[] pColumns = {DB.LOCATION.TIME}; // First we export the location Cursor cursor = getDB().query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId + " AND " + DB.LOCATION.LATITUDE + " IS NOT NULL", null, null, null, null); if (cursor.getCount() > 0) { neededSources.add(DataSourceType.ACTIVITY_LOCATION); neededSources.add(DataSourceType.LOCATION_SUMMARY); } cursor.close(); // Than if present the heart rate cursor = getDB().query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId + " AND " + DB.LOCATION.HR + " IS NOT NULL", null, null, null, null); if (cursor.getCount() > 0) { neededSources.add(DataSourceType.ACTIVITY_HEARTRATE); neededSources.add(DataSourceType.HEARTRATE_SUMMARY); } cursor.close(); // Next will be the speed cursor = getDB().query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId + " AND " + DB.LOCATION.SPEED + " IS NOT NULL", null, null, null, null); if (cursor.getCount() > 0) { neededSources.add(DataSourceType.ACTIVITY_SPEED); neededSources.add(DataSourceType.SPEED_SUMMARY); } cursor.close(); // At last the segments and summary neededSources.add(DataSourceType.ACTIVITY_SEGMENT); neededSources.add(DataSourceType.ACTIVITY_SUMMARY); return neededSources; } public final void exportDataSource(DataSourceType type, Writer writer) { JsonWriter w = new JsonWriter(writer); try { w.beginObject(); w.name("dataStreamId").value(type.getDataStreamId(this)); w.name("dataStreamName").value(type.getDataName()); w.name("type").value(type.SOURCE_TYPE); w.name("dataType"); w.beginObject(); w.name("name").value(type.getDataType()); w.name("field"); w.beginArray(); fillFieldArray(type, w); w.endArray(); w.endObject(); w.name("application"); addApplicationObject(w); w.endObject(); Log.i(getClass().getName(), "Creating new dataSource: " + type.getDataStreamId(this)); } catch (IOException e) { e.printStackTrace(); } } public final String exportTypeData(DataSourceType source, long activityId, StringWriter w) { String requestUrl = ""; switch (source) { case ACTIVITY_SEGMENT: requestUrl = exportActivitySegments(source, activityId, w); return requestUrl; case ACTIVITY_SUMMARY: requestUrl = exportActivitySummary(source, activityId, w); return requestUrl; case ACTIVITY_LOCATION: case ACTIVITY_HEARTRATE: case ACTIVITY_SPEED: case LOCATION_SUMMARY: case HEARTRATE_SUMMARY: case SPEED_SUMMARY: requestUrl = exportSourceDataPoints(source, activityId, w); return requestUrl; } return requestUrl; } private String exportActivitySegments(DataSourceType source, long activityId, StringWriter writer) { String[] pColumns = { DB.ACTIVITY.START_TIME, DB.ACTIVITY.TIME, DB.ACTIVITY.SPORT }; Cursor cursor = getDB().query(DB.ACTIVITY.TABLE, pColumns, "_id = " + activityId, null, null, null, null); cursor.moveToFirst(); //time as nanos long startTime = cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) * SECONDS_TO_NANOS; long endTime = (cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) + cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.TIME))) * SECONDS_TO_NANOS; JsonWriter w = new JsonWriter(writer); try { w.beginObject(); w.name("minStartTimeNs").value(startTime); w.name("maxEndTimeNs").value(endTime); w.name("dataSourceId").value(source.getDataStreamId(this)); w.name("point"); w.beginArray(); //export points w.beginObject(); w.name("startTimeNanos").value(startTime); w.name("endTimeNanos").value(endTime); w.name("dataTypeName").value(source.getDataType()); w.name("originDataSourceId").value(source.getDataStreamId(this)); w.name("value"); w.beginArray(); w.beginObject(); w.name("intVal").value(ACTIVITY_TYPE.get(Sport.valueOf((cursor.getInt(2))))); w.endObject(); w.endArray(); w.name("rawTimestampNanos").value(startTime); w.name("computationTimeMillis").value(System.currentTimeMillis()); w.endObject(); //end export points w.endArray(); w.endObject(); } catch (IOException e) { e.printStackTrace(); } cursor.close(); return getDataSetURLSuffix(source, startTime, endTime); } private String exportSourceDataPoints(DataSourceType source, long activityId, StringWriter writer) { ArrayList<String> pColumns = new ArrayList<String>(); pColumns.add("MIN(" + DB.LOCATION.TIME + ") AS MIN"); pColumns.add("MAX(" + DB.LOCATION.TIME + ")"); Cursor minMaxTime = getDB().query(DB.LOCATION.TABLE, pColumns.toArray(new String[pColumns.size()]), DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, null); minMaxTime.moveToFirst(); pColumns = new ArrayList<String>(); pColumns.add(DB.LOCATION.TIME); List<DataTypeField> fields = DATA_TYPE_FIELDS.get(source); for (DataTypeField field : fields) { pColumns.add(field.getColumn()); } Cursor cursor = getDB().query(DB.LOCATION.TABLE, pColumns.toArray(new String[pColumns.size()]), DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, null); cursor.moveToFirst(); long startTime = minMaxTime.getLong(0) * MICRO_TO_NANOS; long endTime = minMaxTime.getLong(1) * MICRO_TO_NANOS; minMaxTime.close(); JsonWriter w = new JsonWriter(writer); try { w.beginObject(); w.name("minStartTimeNs").value(startTime); w.name("maxEndTimeNs").value(endTime); w.name("dataSourceId").value(source.getDataStreamId(this)); w.name("point"); w.beginArray(); //export points do { w.beginObject(); w.name("startTimeNanos").value(cursor.getLong(cursor.getColumnIndex(DB.LOCATION.TIME)) * MICRO_TO_NANOS); if (!cursor.isLast()) { cursor.moveToNext(); w.name("endTimeNanos").value(cursor.getLong(cursor.getColumnIndex(DB.LOCATION.TIME)) * MICRO_TO_NANOS); cursor.moveToPrevious(); } else { w.name("endTimeNanos").value(endTime); } w.name("originDataSourceId").value(source.getDataStreamId(this) ); w.name("dataTypeName").value(source.getDataType()); w.name("value"); w.beginArray(); writeDataPointValues(fields, cursor, w); w.endArray(); w.name("rawTimestampNanos").value(cursor.getLong(cursor.getColumnIndex(DB.LOCATION.TIME)) * MICRO_TO_NANOS); w.name("computationTimeMillis").value(System.currentTimeMillis()); w.endObject(); } while (cursor.moveToNext()); //end export points w.endArray(); w.endObject(); } catch (IOException e) { e.printStackTrace(); } cursor.close(); return getDataSetURLSuffix(source, startTime, endTime); } private String exportActivitySummary(DataSourceType source, long activityId, StringWriter writer) { ArrayList<String> pColumns = new ArrayList<String>(); pColumns.add(DB.ACTIVITY.START_TIME); pColumns.add(DB.ACTIVITY.TIME); List<DataTypeField> fields = DATA_TYPE_FIELDS.get(source); for (DataTypeField field : fields) { pColumns.add(field.getColumn()); } Cursor cursor = getDB().query(DB.ACTIVITY.TABLE, pColumns.toArray(new String[pColumns.size()]), "_id = " + activityId, null, null, null, null); cursor.moveToFirst(); //time as nanos long startTime = cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) * SECONDS_TO_NANOS; long endTime = (cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) + cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.TIME))) * SECONDS_TO_NANOS; JsonWriter w = new JsonWriter(writer); try { w.beginObject(); w.name("minStartTimeNs").value(startTime); w.name("maxEndTimeNs").value(endTime); w.name("dataSourceId").value(source.getDataStreamId(this)); w.name("point"); w.beginArray(); //export points w.beginObject(); w.name("startTimeNanos").value(startTime); w.name("endTimeNanos").value(endTime); w.name("dataTypeName").value(source.getDataType()); w.name("originDataSourceId").value(source.getDataStreamId(this)); w.name("value"); w.beginArray(); writeDataPointValues(fields, cursor, w); w.endArray(); w.name("rawTimestampNanos").value(startTime); w.name("computationTimeMillis").value(System.currentTimeMillis()); w.endObject(); //end export points w.endArray(); w.endObject(); } catch (IOException e) { e.printStackTrace(); } return getDataSetURLSuffix(source, startTime, endTime); } private void addApplicationObject(JsonWriter w) throws IOException { w.beginObject(); w.name("name").value(mContext.getString(R.string.app_name)); w.endObject(); } private void fillFieldArray(DataSourceType type, JsonWriter w) throws IOException { for (DataTypeField field : DATA_TYPE_FIELDS.get(type)) { w.beginObject(); w.name(field.getNameValue().first).value(field.getNameValue().second); w.name(field.getFormatSourceValue().first).value(field.getFormatSourceValue().second); w.endObject(); } } private void writeDataPointValues(List<DataTypeField> fields, Cursor cursor, JsonWriter w) throws IOException { for (DataTypeField field : fields) { w.beginObject(); w.name(field.getFormatDataPointValue()); if (field.getFormatDataPointValue().equals("intVal")) { w.value(cursor.getInt(cursor.getColumnIndex(field.getColumn()))); } else if (field.getFormatDataPointValue().equals("fpVal")) { w.value(cursor.getDouble(cursor.getColumnIndex(field.getColumn()))); } w.endObject(); } } public final String exportSession(long activityId, Writer writer) { String[] pColumns = { DB.ACTIVITY.START_TIME, DB.ACTIVITY.TIME, DB.ACTIVITY.COMMENT, DB.ACTIVITY.SPORT }; Cursor cursor = getDB().query(DB.ACTIVITY.TABLE, pColumns, "_id = " + activityId, null, null, null, null); cursor.moveToFirst(); long startTime = cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) * SECONDS_TO_MILLIS; long endTime = (cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.START_TIME)) + cursor.getLong(cursor.getColumnIndex(DB.ACTIVITY.TIME))) * SECONDS_TO_MILLIS; String[] sports = mContext.getResources().getStringArray(R.array.sportEntries); JsonWriter w = new JsonWriter(writer); try { w.beginObject(); w.name("id").value(mContext.getString(R.string.app_name) + "-" + startTime + "-" + endTime); w.name("name").value((cursor.getInt(cursor.getColumnIndex(DB.ACTIVITY.SPORT)) == 0 ? sports[0] : sports[1]) + ": " + getWorkoutName(startTime)); w.name("description").value(cursor.getString(cursor.getColumnIndex(DB.ACTIVITY.COMMENT))); //comment w.name("startTimeMillis").value(startTime); w.name("endTimeMillis").value(endTime); w.name("application"); addApplicationObject(w); w.name("activityType").value(ACTIVITY_TYPE.get(Sport.valueOf(cursor.getInt(cursor.getColumnIndex(DB.ACTIVITY.SPORT))))); w.endObject(); } catch (IOException e) { e.printStackTrace(); } cursor.close(); return getSessionURLSuffix(startTime, endTime); } private String getWorkoutName(long startTime) { return DateFormat.getInstance().format(new Date(startTime)); } private String getDataSetURLSuffix(DataSourceType source, long startTime, long endTime) { return GoogleFitSynchronizer.REST_DATASOURCE + "/" + SyncHelper.URLEncode(source.getDataStreamId(this)) + "/" + GoogleFitSynchronizer.REST_DATASETS + "/" + startTime + "-" + endTime; } private String getSessionURLSuffix(long startTime, long endTime) { return GoogleFitSynchronizer.REST_SESSIONS + "/" + mContext.getString(R.string.app_name) + "-" + startTime + "-" + endTime; } public enum DataSourceType { ACTIVITY_SEGMENT("com.google.activity.segment", "runnerup_activity_segment"), ACTIVITY_HEARTRATE("com.google.heart_rate.bpm", "runnerup_activity_heartrate"), ACTIVITY_LOCATION("com.google.location.sample", "runnerup_activity_location"), ACTIVITY_SPEED("com.google.speed", "runnerup_activity_speed"), ACTIVITY_SUMMARY("com.google.activity.summary", "runnerup_activity_summary"), HEARTRATE_SUMMARY("com.google.heart_rate.summary", "runnerup_heartrate_summary"), LOCATION_SUMMARY("com.google.location.bounding_box", "runnerup_location_summary"), SPEED_SUMMARY("com.google.speed.summary", "runnerup_speed_summary"); static final String SOURCE_TYPE = "derived"; private final String dataType; private final String dataName; DataSourceType(String type, String name) { this.dataType = type; this.dataName = name; } public String getDataStreamId(GoogleFitData gfd) { return SOURCE_TYPE + ":" + getDataType() + ":" + getProjectId(gfd) + ":" + getDataName(); } public String getProjectId(GoogleFitData gfd) { return gfd.getProjectId(); } public String getDataType() { return dataType; } public String getDataName() { return dataName; } } private static class DataTypeField { static final String NAME = "name"; static final String FORMAT = "format"; private Pair<String, String> nameValue = null; private Pair<String, String> formatSourceValue = null; private String formatDataPointValue = null; private String column = null; public DataTypeField(String name, Pair<String, String> format, String dbColumn) { this.setNameValue(Pair.create(NAME, name)); this.setFormatSourceValue(Pair.create(FORMAT, format.first)); this.setFormatDataPointValue(format.second); this.setColumn(dbColumn); } public Pair<String, String> getNameValue() { return nameValue; } public void setNameValue(Pair<String, String> value) { this.nameValue = value; } public Pair<String, String> getFormatSourceValue() { return formatSourceValue; } public void setFormatSourceValue(Pair<String, String> value) { this.formatSourceValue = value; } public String getFormatDataPointValue() { return formatDataPointValue; } public void setFormatDataPointValue(String value) { this.formatDataPointValue = value; } public String getColumn() { return column; } public void setColumn(String name) { this.column = name; } } }