Merge heads

This commit is contained in:
youtaol
2012-03-13 17:13:33 +08:00
40 changed files with 1608 additions and 790 deletions
@@ -17,7 +17,6 @@ package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TracksColumns;
@@ -27,6 +26,7 @@ import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -92,11 +92,6 @@ public class MyTracks extends TabActivity implements OnTouchListener {
*/
private MyTracksProviderUtils providerUtils;
/**
* Google Analytics tracker
*/
private GoogleAnalyticsTracker tracker;
/*
* Tabs/View navigation:
*/
@@ -144,12 +139,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
tracker = GoogleAnalyticsTracker.getInstance();
// Start the tracker in manual dispatch mode...
tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext());
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
tracker.trackPageView("/appstart");
tracker.dispatch();
AnalyticsUtils.sendPageViews(this, "/appstart");
providerUtils = MyTracksProviderUtils.Factory.get(this);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
@@ -256,8 +246,6 @@ public class MyTracks extends TabActivity implements OnTouchListener {
protected void onStop() {
Log.d(TAG, "MyTracks.onStop");
dataHub.stop();
tracker.dispatch();
tracker.stop();
super.onStop();
}
@@ -207,9 +207,8 @@ public class SearchActivity extends ListActivity {
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(CATEGORY_FIELD, track.getCategory());
resultMap.put(TIME_FIELD, StringUtils.formatDateTime(this, stats.getStartTime()));
resultMap.put(STATS_FIELD,
StringUtils.formatTimeDistance(this, stats.getTotalDistance(), stats.getTotalTime(),
metricUnits));
resultMap.put(STATS_FIELD, StringUtils.formatTimeDistance(
this, stats.getTotalTime(), stats.getTotalDistance(), metricUnits));
resultMap.put(TRACK_ID_FIELD, track.getId());
}
@@ -349,7 +349,8 @@ public class TrackList extends ListActivity
} else if (columnIndex == totalDistanceIdx) {
double totalDistance = cursor.getDouble(totalDistanceIdx);
long totalTime = cursor.getLong(totalTimeIdx);
String totalDistanceStr = StringUtils.formatTimeDistance(TrackList.this, totalDistance, totalTime, metricUnits);
String totalDistanceStr = StringUtils.formatTimeDistance(
TrackList.this, totalTime, totalDistance, metricUnits);
textView.setText(totalDistanceStr);
} else {
textView.setText(cursor.getString(columnIndex));
@@ -300,8 +300,8 @@ public class SendDocsUtils {
appendTag(builder, "name", track.getName());
appendTag(builder, "description", track.getDescription());
appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
appendTag(builder, "totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()));
appendTag(builder, "movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()));
appendTag(builder, "totaltime", StringUtils.formatElapsedTimeWithHour(stats.getTotalTime()));
appendTag(builder, "movingtime", StringUtils.formatElapsedTimeWithHour(stats.getMovingTime()));
appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
appendTag(builder, "distanceunit", distanceUnit);
appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
@@ -335,7 +335,7 @@ public class SendDocsUtils {
.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.stringAsCData(value))
.append(StringUtils.formatCData(value))
.append("</gsx:")
.append(name)
.append(">");
@@ -15,31 +15,37 @@
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Date;
/**
* Exports a track as a CSV file, according to RFC 4180.
*
* The first field is a type:
* TRACK - track description
* P - point
* WAYPOINT - waypoint
*
* For each type, the fields are:
*
* TRACK,,,,,,,,name,description,
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
* Write track as CSV to a file. See RFC 4180 for info on CSV. Output three
* tables.<br>
* The first table contains the track info. Its columns are:<br>
* "Track name","Activity type","Track description" <br>
* <br>
* The second table contains the markers. Its columns are:<br>
* "Marker name","Marker type","Marker description","Latitude (deg)","Longitude
* (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"<br>
* <br>
* The thrid table contains the points. Its columns are:<br>
* "Segment","Point","Latitude (deg)","Longitude (deg)","Altitude (m)","Bearing
* (deg)","Accuracy (m)","Speed (m/s)","Time","Power (W)","Cadence (rpm)","Heart
* rate (bpm)","Battery level (%)"<br>
*
* @author Rodrigo Damazio
*/
@@ -51,121 +57,41 @@ public class CsvTrackWriter implements TrackFormatWriter {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
private int segmentIdx = 0;
private int numFields = -1;
private PrintWriter pw;
private final Context context;
private PrintWriter printWriter;
private Track track;
private int segmentIndex;
private int pointIndex;
public CsvTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
public void prepare(Track aTrack, OutputStream out) {
track = aTrack;
printWriter = new PrintWriter(out);
segmentIndex = 0;
pointIndex = 0;
}
@Override
public void close() {
printWriter.close();
}
@Override
public void writeHeader() {
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
}
@Override
public void writeBeginTrack(Location firstPoint) {
writeCommaSeparatedLine("TRACK",
null, null, null, null, null, null, null,
track.getName(), track.getDescription(),
null);
}
@Override
public void writeOpenSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("P",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
null, null,
Integer.toString(segmentIdx));
}
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("WAYPOINT",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
waypoint.getName(),
waypoint.getDescription(),
null);
}
/**
* Writes a single line of a comma-separated-value file.
*
* @param strs the values to be written as comma-separated
*/
private void writeCommaSeparatedLine(String... strs) {
if (numFields == -1) {
numFields = strs.length;
} else if (strs.length != numFields) {
throw new IllegalArgumentException(
"CSV lines with different number of fields");
}
boolean isFirst = true;
for (String str : strs) {
if (!isFirst) {
pw.print(',');
}
isFirst = false;
if (str != null) {
pw.print('"');
pw.print(str.replaceAll("\"", "\"\""));
pw.print('"');
}
}
pw.println();
}
@Override
public void writeCloseSegment() {
segmentIdx++;
}
@Override
public void writeEndTrack(Location lastPoint) {
// Do nothing
writeCommaSeparatedLine(context.getString(R.string.track_detail_track_name),
context.getString(R.string.track_detail_activity_type_hint),
context.getString(R.string.track_detail_track_description));
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
writeCommaSeparatedLine();
}
@Override
@@ -174,7 +100,144 @@ public class CsvTrackWriter implements TrackFormatWriter {
}
@Override
public void close() {
pw.close();
public void writeBeginWaypoints() {
writeCommaSeparatedLine(context.getString(R.string.marker_detail_marker_name),
context.getString(R.string.marker_detail_marker_type_hint),
context.getString(R.string.marker_detail_marker_description),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_location_time));
}
@Override
public void writeEndWaypoints() {
writeCommaSeparatedLine();
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
writeCommaSeparatedLine(waypoint.getName(),
waypoint.getCategory(),
waypoint.getDescription(),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
StringUtils.formatDateTimeIso8601(location.getTime()));
}
@Override
public void writeBeginTrack(Location firstPoint) {
writeCommaSeparatedLine(context.getString(R.string.description_track_segment),
context.getString(R.string.description_track_point),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_location_time),
context.getString(R.string.description_sensor_power),
context.getString(R.string.description_sensor_cadence),
context.getString(R.string.description_sensor_heart_rate),
context.getString(R.string.description_sensor_battery_level));
}
@Override
public void writeEndTrack(Location lastPoint) {
// Do nothing
}
@Override
public void writeOpenSegment() {
segmentIndex++;
pointIndex = 0;
}
@Override
public void writeCloseSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String power = null;
String cadence = null;
String heartRate = null;
String batteryLevel = null;
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
power = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
cadence = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
heartRate = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasBatteryLevel()) {
SensorData sensorData = sensorDataSet.getBatteryLevel();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
batteryLevel = Double.toString(sensorData.getValue());
}
}
}
}
pointIndex++;
writeCommaSeparatedLine(Integer.toString(segmentIndex),
Integer.toString(pointIndex),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
StringUtils.formatDateTimeIso8601(location.getTime()),
power,
cadence,
heartRate,
batteryLevel);
}
/**
* Writes a single line of a CSV file.
*
* @param values the values to be written as CSV
*/
private void writeCommaSeparatedLine(String... values) {
StringBuilder builder = new StringBuilder();
boolean isFirst = true;
for (String value : values) {
if (!isFirst) {
builder.append(',');
}
isFirst = false;
if (value != null) {
builder.append('"');
builder.append(value.replaceAll("\"", "\"\""));
builder.append('"');
}
}
printWriter.println(builder.toString());
}
}
@@ -450,7 +450,7 @@ public class GpxImporter extends DefaultHandler {
// Parse the time
long time;
try {
time = StringUtils.parseXmlDateTime(content.trim());
time = StringUtils.getTime(content.trim());
} catch (IllegalArgumentException e) {
String msg = createErrorMessage("Unable to parse time: " + content);
throw new SAXException(msg, e);
@@ -18,7 +18,6 @@ package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
@@ -94,8 +93,8 @@ public class GpxTrackWriter implements TrackFormatWriter {
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<metadata>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.stringAsCData(track.getDescription()) + "</desc>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
}
@@ -111,8 +110,8 @@ public class GpxTrackWriter implements TrackFormatWriter {
public void writeBeginTrack(Location firstLocation) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.stringAsCData(track.getDescription()) + "</desc>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@@ -139,8 +138,7 @@ public class GpxTrackWriter implements TrackFormatWriter {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println(
"<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</time>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@@ -162,11 +160,10 @@ public class GpxTrackWriter implements TrackFormatWriter {
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println(
"<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.stringAsCData(waypoint.getName()) + "</name>");
printWriter.println(
"<desc>" + StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("</wpt>");
}
}
@@ -24,7 +24,6 @@ import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
@@ -43,6 +42,11 @@ import java.util.ArrayList;
*/
public class KmlTrackWriter implements TrackFormatWriter {
/**
* ID of the KML feature to play a tour.
*/
public static final String TOUR_FEATURE_ID = "tour";
private static final String WAYPOINT_STYLE = "waypoint";
private static final String STATISTICS_STYLE = "statistics";
private static final String START_STYLE = "start";
@@ -54,6 +58,17 @@ public class KmlTrackWriter implements TrackFormatWriter {
private static final String POWER = "power";
private static final String BATTER_LEVEL = "battery_level";
private static final String WAYPOINT_ICON =
"http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String STATISTICS_ICON =
"http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String START_ICON =
"http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON =
"http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON =
"http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final DescriptionGenerator descriptionGenerator;
private Track track;
@@ -107,20 +122,16 @@ public class KmlTrackWriter implements TrackFormatWriter {
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
printWriter.println(
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>" + StringUtils.stringAsCData(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>" + StringUtils.formatCData(
context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(
START_STYLE, "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png", 32, 1);
writePlacemarkerStyle(
END_STYLE, "http://maps.google.com/mapfiles/kml/paddle/red-circle.png", 32, 1);
writePlacemarkerStyle(
STATISTICS_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", 20, 2);
writePlacemarkerStyle(
WAYPOINT_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png", 20, 2);
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
@@ -142,7 +153,7 @@ public class KmlTrackWriter implements TrackFormatWriter {
public void writeBeginWaypoints() {
if (printWriter != null) {
printWriter.println(
"<Folder><name>" + StringUtils.stringAsCData(context.getString(R.string.menu_markers))
"<Folder><name>" + StringUtils.formatCData(context.getString(R.string.menu_markers))
+ "</name>");
}
}
@@ -169,10 +180,10 @@ public class KmlTrackWriter implements TrackFormatWriter {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, track.getDescription(), START_STYLE, firstLocation);
printWriter.println("<Placemark>");
printWriter.println("<Placemark id=\"" + TOUR_FEATURE_ID + "\">");
printWriter.println(
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
@@ -232,8 +243,7 @@ public class KmlTrackWriter implements TrackFormatWriter {
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println(
"<when>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</when>");
printWriter.println("<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println(
"<gx:coord>" + location.getLongitude() + " " + location.getLatitude() + " "
+ location.getAltitude() + "</gx:coord>");
@@ -309,8 +319,8 @@ public class KmlTrackWriter implements TrackFormatWriter {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println(
"<description>" + StringUtils.stringAsCData(description) + "</description>");
printWriter.println("<name>" + StringUtils.stringAsCData(name) + "</name>");
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
printWriter.println("<Point>");
printWriter.println(
@@ -325,9 +335,13 @@ public class KmlTrackWriter implements TrackFormatWriter {
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\"><LineStyle>");
printWriter.println("<color>7f0000ff</color><width>4</width>");
printWriter.println("</LineStyle></Style>");
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
/**
@@ -356,7 +370,7 @@ public class KmlTrackWriter implements TrackFormatWriter {
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println(
"<displayName>" + StringUtils.stringAsCData(displayName) + "</displayName>");
"<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
}
@@ -159,9 +159,11 @@ public class SaveActivity extends Activity {
Uri uri = Uri.fromFile(new File(writer.getAbsolutePath()));
Intent intent = new Intent()
.setClassName(PlayTrackUtils.GOOGLE_EARTH_PACKAGE, PlayTrackUtils.GOOGLE_EARTH_CLASS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setDataAndType(uri, PlayTrackUtils.KML_MIME_TYPE);
.setDataAndType(uri, PlayTrackUtils.KML_MIME_TYPE)
.putExtra(PlayTrackUtils.TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID);
startActivity(intent);
}
@@ -22,86 +22,89 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.lib.R;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.Locale;
/**
* Write out a a track in the Garmin training center database, tcx format.
* As defined by:
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
*
* The TCX file written by this class has been verified as compatible with
* Garmin Training Center 3.5.3.
* Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
* for info on TCX. <br>
* The TCX file output is verified by uploading the file to
* http://connect.garmin.com/.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class TcxTrackWriter implements TrackFormatWriter {
// "Biking" related string IDs
/**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
private final String name;
private SportType(String name) {
this.name = name;
}
/**
* Gets the name of the sport type
*/
public String getName() {
return name;
}
}
// My Tracks categories that are considered as TCX biking sport type.
private static final int TCX_SPORT_BIKING_IDS[] = {
R.string.activity_type_cycling,
R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_biking,
R.string.activity_type_road_biking,
R.string.activity_type_track_cycling,
};
R.string.activity_type_track_cycling };
// "Running" related string IDs
// My Tracks categories that are considered as TCX running sport type.
private static final int TCX_SPORT_RUNNING_IDS[] = {
R.string.activity_type_running,
R.string.activity_type_speed_walking,
R.string.activity_type_street_running,
R.string.activity_type_track_running,
R.string.activity_type_trail_running,
R.string.activity_type_walking,
};
// These are the only sports allowed by the TCX v2 specification for fields
// of type Sport_t.
private static final String TCX_SPORT_BIKING = "Biking";
private static final String TCX_SPORT_RUNNING = "Running";
private static final String TCX_SPORT_OTHER = "Other";
// Values for fields of type Build_t/Type.
private static final String TCX_TYPE_RELEASE = "Release";
private static final String TCX_TYPE_INTERNAL = "Internal";
R.string.activity_type_walking };
private final Context context;
private PrintWriter pw = null;
private Track track;
// Determines whether to encode cadence value as running or cycling cadence.
private boolean sportIsCycling;
private PrintWriter printWriter;
private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING);
public void prepare(Track aTrack, OutputStream out) {
this.track = aTrack;
this.printWriter = new PrintWriter(out);
this.sportType = getSportType(track.getCategory());
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
if (printWriter != null) {
printWriter.close();
printWriter = null;
}
}
@@ -112,246 +115,214 @@ public class TcxTrackWriter implements TrackFormatWriter {
@Override
public void writeHeader() {
if (pw == null) {
return;
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<TrainingCenterDatabase"
+ " xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation="
+ "\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
+ " http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("<Author xsi:type=\"Application_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <Build>, <LangID>, and <PartNumber> are required by type=Application_t.
printWriter.println("<Build>");
writeVersion();
printWriter.println("</Build>");
printWriter.println("<LangID>" + Locale.getDefault().getLanguage() + "</LangID>");
printWriter.println("<PartNumber>000-00000-00</PartNumber>");
printWriter.println("</Author>");
printWriter.println("</TrainingCenterDatabase>");
}
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
pw.println();
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw == null) {
return;
}
if (printWriter != null) {
String startTime = StringUtils.formatDateTimeIso8601(track.getStatistics().getStartTime());
long totalTimeInSeconds = track.getStatistics().getTotalTime() / 1000;
String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime());
pw.println(" <Activities>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>");
pw.print(" <DistanceMeters>");
pw.print(track.getStatistics().getTotalDistance());
pw.println("</DistanceMeters>");
// TODO max speed etc.
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
@Override
public void writeLocation(Location location) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(location.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(location.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(location.getAltitude());
pw.println("</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData != null) {
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
pw.print(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
boolean cadenceAvailable = sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue();
// TCX Trackpoint_t contains a sequence. Thus, the legacy XML element
// <Cadence> needs to be put before <Extensions>.
// This field should only be used for the case that activity was marked as biking.
// Otherwise cadence is interpreted as running cadence data which
// is written in the <Extensions> as <RunCadence>.
if (sportIsCycling && cadenceAvailable) {
pw.print(" <Cadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
boolean powerAvailable = sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue();
if(powerAvailable || (!sportIsCycling && cadenceAvailable)) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// RunCadence needs to be put before power in order to be understood
// by Garmin Training Center.
if (!sportIsCycling && cadenceAvailable) {
pw.print("<RunCadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.print("</RunCadence>");
}
if (powerAvailable) {
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
}
pw.println("</TPX></Extensions>");
}
}
}
pw.println(" </Trackpoint>");
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
printWriter.println("<Activities>");
printWriter.println("<Activity Sport=\"" + sportType.getName() + "\">");
printWriter.println("<Id>" + startTime + "</Id>");
printWriter.println("<Lap StartTime=\"" + startTime + "\">");
printWriter.println("<TotalTimeSeconds>" + totalTimeInSeconds + "</TotalTimeSeconds>");
printWriter.println("<DistanceMeters>" + track.getStatistics().getTotalDistance()
+ "</DistanceMeters>");
// <Calories> is required, just put in 0.
printWriter.println("<Calories>0</Calories>");
printWriter.println("<Intensity>Active</Intensity>");
printWriter.println("<TriggerMethod>Manual</TriggerMethod>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw == null) {
return;
if (printWriter != null) {
printWriter.println("</Lap>");
printWriter.println("<Creator xsi:type=\"Device_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <UnitId>, <ProductID>, and <Version> are required for type=Device_t.
printWriter.println("<UnitId>0</UnitId>");
printWriter.println("<ProductID>0</ProductID>");
writeVersion();
printWriter.println("</Creator>");
printWriter.println("</Activity>");
printWriter.println("</Activities>");
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
writeVersion();
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
}
@Override
public void writeFooter() {
if (pw == null) {
return;
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<Track>");
}
pw.println(" <Author xsi:type=\"Application_t\">");
}
// We put the version in the name because there isn't a better place for
// it. The TCX schema tightly defined the Version tag, so we can't put it
// there. They've similarly constrained the PartNumber tag, so it can't go
// there either.
pw.format("<Name>My Tracks %s by Google</Name>\n", SystemUtils.getMyTracksVersion(context));
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("</Track>");
}
}
pw.println("<Build>");
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<Trackpoint>");
printWriter.println("<Time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</Time>");
printWriter.println("<Position>");
printWriter.println("<LatitudeDegrees>" + location.getLatitude() + "</LatitudeDegrees>");
printWriter.println("<LongitudeDegrees>" + location.getLongitude() + "</LongitudeDegrees>");
printWriter.println("</Position>");
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
writeVersion();
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
boolean cadenceAvailable = sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
boolean powerAvailable = sensorDataSet.hasPower()
&& sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
if (heartRateAvailable) {
printWriter.println("<HeartRateBpm>");
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
printWriter.println("</HeartRateBpm>");
}
pw.format("<Type>%s</Type>\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
// <Cadence> needs to be put before <Extensions>.
// According to the TCX spec, <Cadence> is only for the biking sport
// type. For others, use <RunCadence> in <Extensions>.
if (cadenceAvailable && sportType == SportType.BIKING) {
// The spec requires the max value be 254.
printWriter.println(
"<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
}
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
printWriter.println("<Extensions>");
printWriter.println(
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// <RunCadence> needs to be put before <Watts>.
if (cadenceAvailable && sportType != SportType.BIKING) {
// The spec requires the max value to be 254.
printWriter.println("<RunCadence>"
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
}
if (powerAvailable) {
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
}
printWriter.println("</TPX>");
printWriter.println("</Extensions>");
}
}
}
printWriter.println("</Trackpoint>");
}
}
@Override
public void writeBeginWaypoints() {
// Do nothing
// Do nothing.
}
@Override
public void writeEndWaypoints() {
// Do nothing
// Do nothing.
}
@Override
public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere.
// Do nothing.
}
/**
* Writes the TCX Version.
*/
private void writeVersion() {
if (pw == null) {
return;
}
// Split the My Tracks version code into VersionMajor, VersionMinor, and,
// BuildMajor to fit the integer type requirement for these fields in the
// TCX spec.
String[] versionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
int versionMajor = versionComponents.length > 0 ? Integer.valueOf(versionComponents[0]) : 0;
int versionMinor = versionComponents.length > 1 ? Integer.valueOf(versionComponents[1]) : 0;
int buildMajor = versionComponents.length > 2 ? Integer.valueOf(versionComponents[2]) : 0;
// Splitting the myTracks version code into VersionMajor, VersionMinor and BuildMajor
// to fit the integer type requirement for these fields in the TCX spec.
// Putting a string like "x.x.x" into VersionMajor breaks XML validation.
// We also set the BuildMinor version to 1 if this is a development build to
// signify that this build is newer than the one associated with the
// version code given in BuildMajor.
String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", Integer.valueOf(myTracksVersionComponents[0]));
pw.format("<VersionMinor>%d</VersionMinor>\n", Integer.valueOf(myTracksVersionComponents[1]));
// TCX schema says these are optional but http://connect.garmin.com only accepts
// the TCX file when they are present.
pw.format("<BuildMajor>%d</BuildMajor>\n", Integer.valueOf(myTracksVersionComponents[2]));
pw.format("<BuildMinor>%d</BuildMinor>\n", SystemUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
printWriter.println("<Version>");
printWriter.println("<VersionMajor>" + versionMajor + "</VersionMajor>");
printWriter.println("<VersionMinor>" + versionMinor + "</VersionMinor>");
// According to TCX spec, these are optional. But http://connect.garmin.com
// requires them.
printWriter.println("<BuildMajor>" + buildMajor + "</BuildMajor>");
printWriter.println("<BuildMinor>0</BuildMinor>");
printWriter.println("</Version>");
}
private String categoryToTcxSport(String category) {
/**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim();
// category is possibly localized here, so compare it to localized string resources
// For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) {
if (category.equalsIgnoreCase(context.getResources().getString(i))) {
return TCX_SPORT_RUNNING;
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.RUNNING;
}
}
for (int i : TCX_SPORT_BIKING_IDS) {
if (category.equalsIgnoreCase(context.getResources().getString(i))) {
return TCX_SPORT_BIKING;
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.BIKING;
}
}
// for tracks without localized activity type
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
return TCX_SPORT_RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
return TCX_SPORT_BIKING;
// For tracks without localized category.
if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
return SportType.RUNNING;
} else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
return SportType.BIKING;
} else {
return TCX_SPORT_OTHER;
return SportType.OTHER;
}
}
}
@@ -50,7 +50,7 @@ public class TrackWriterFactory {
CSV {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new CsvTrackWriter();
return new CsvTrackWriter(context);
}
},
TCX {
@@ -21,6 +21,7 @@ import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
@@ -45,18 +46,23 @@ public class SendFusionTablesActivity extends AbstractSendActivity {
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setFusionTablesSuccess(success);
Class<?> next;
if (isCancel) {
next = UploadResultActivity.class;
} else {
if (sendRequest.isSendDocs()) {
next = SendDocsActivity.class;
} else {
next = UploadResultActivity.class;
}
}
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = new Intent(this, next).putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -454,7 +454,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = uploaded / total;
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.app.AlertDialog;
@@ -46,12 +47,14 @@ import java.util.ArrayList;
public class ChooseMapActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
private static final int ERROR_DIALOG = 2;
@VisibleForTesting
static final int ERROR_DIALOG = 2;
private SendRequest sendRequest;
private ChooseMapAsyncTask asyncTask;
private ProgressDialog progressDialog;
private ArrayAdapter<ListItem> arrayAdapter;
@VisibleForTesting
ArrayAdapter<ListItem> arrayAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -166,7 +169,7 @@ public class ChooseMapActivity extends Activity {
*/
public void onAsyncTaskCompleted(
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
removeDialog(PROGRESS_DIALOG);
removeProgressDialog();
if (success) {
arrayAdapter.clear();
// To prevent displaying the emptyView message momentarily before the
@@ -181,7 +184,7 @@ public class ChooseMapActivity extends Activity {
}
}
} else {
showDialog(ERROR_DIALOG);
showErrorDialog();
}
}
@@ -191,6 +194,22 @@ public class ChooseMapActivity extends Activity {
public void showProgressDialog() {
showDialog(PROGRESS_DIALOG);
}
/**
* Shows the error dialog.
*/
@VisibleForTesting
void showErrorDialog() {
showDialog(ERROR_DIALOG);
}
/**
* Remove the progress dialog.
*/
@VisibleForTesting
void removeProgressDialog() {
removeDialog(PROGRESS_DIALOG);
}
/**
* Starts the next activity, {@link SendMapsActivity}.
@@ -210,7 +229,8 @@ public class ChooseMapActivity extends Activity {
*
* @author Jimmy Shih
*/
private class ListItem {
@VisibleForTesting
class ListItem {
private String mapId;
private MapsMapMetadata mapData;
@@ -233,4 +253,4 @@ public class ChooseMapActivity extends Activity {
return mapData;
}
}
}
}
@@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
@@ -47,20 +48,25 @@ public class SendMapsActivity extends AbstractSendActivity {
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setMapsSuccess(success);
Class<?> next;
if (isCancel) {
next = UploadResultActivity.class;
} else {
if (sendRequest.isSendFusionTables()) {
next = SendFusionTablesActivity.class;
} else if (sendRequest.isSendDocs()) {
next = SendDocsActivity.class;
} else {
next = UploadResultActivity.class;
}
}
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = new Intent(this, next).putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (request.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -472,8 +472,8 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = uploaded / total;
double scaledPercentage = totalPercentage
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
@@ -109,7 +109,7 @@ public class AccountChooserActivity extends Activity {
super.onResume();
if (accounts.length == 0) {
showDialog(NO_ACCOUNT_DIALOG);
} else {
} else if (accounts.length > 1 ) {
showDialog(CHOOSE_ACCOUNT_DIALOG);
}
}
@@ -15,10 +15,9 @@
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
@@ -39,6 +38,8 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.TableRow;
import java.util.ArrayList;
/**
* A chooser to select the Google services to upload a track to.
*
@@ -246,20 +247,17 @@ public class UploadServiceChooserActivity extends Activity {
* Sends stats to Google Analytics.
*/
private void sendStats() {
GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext());
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
ArrayList<String> pages = new ArrayList<String>();
if (sendRequest.isSendMaps()) {
tracker.trackPageView("/send/maps");
pages.add("/send/maps");
}
if (sendRequest.isSendFusionTables()) {
tracker.trackPageView("/send/fusion_tables");
pages.add("/send/fusion_tables");
}
if (sendRequest.isSendDocs()) {
tracker.trackPageView("/send/docs");
pages.add("/send/docs");
}
tracker.dispatch();
tracker.stop();
AnalyticsUtils.sendPageViews(this, pages.toArray(new String[pages.size()]));
}
@VisibleForTesting
@@ -0,0 +1,43 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.maps.mytracks.R;
import android.content.Context;
/**
* Utitlites for sending pageviews to Google Analytics.
*
* @author Jimmy Shih
*/
public class AnalyticsUtils {
private AnalyticsUtils() {}
public static void sendPageViews(Context context, String ... pages) {
GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
tracker.start(context.getString(R.string.my_tracks_analytics_id), context);
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(context));
for (String page : pages) {
tracker.trackPageView(page);
}
tracker.dispatch();
tracker.stop();
}
}
@@ -21,8 +21,6 @@ import com.google.common.annotations.VisibleForTesting;
import android.os.Environment;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
/**
* Utilities for dealing with files.
@@ -38,16 +36,6 @@ public class FileUtils {
@VisibleForTesting
static final int MAX_FAT32_PATH_LENGTH = 260;
/**
* Timestamp format in UTC time zone.
*/
public static final SimpleDateFormat FILE_TIMESTAMP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
FILE_TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
/**
* Builds a path inside the My Tracks directory in the SD card.
*
@@ -45,14 +45,17 @@ public class PlayTrackUtils {
* KML mime type.
*/
public static final String KML_MIME_TYPE = "application/vnd.google-earth.kml+xml";
private static final String GOOGLE_EARTH_PACKAGE = "com.google.earth";
public static final String TOUR_FEATURE_ID = "com.google.earth.EXTRA.tour_feature_id";
public static final String GOOGLE_EARTH_PACKAGE = "com.google.earth";
public static final String GOOGLE_EARTH_CLASS = "com.google.earth.EarthActivity";
private static final String EARTN_MARKET_URI = "market://details?id=" + GOOGLE_EARTH_PACKAGE;
private PlayTrackUtils() {}
/**
* Returns true if Google Earth is installed.
* Returns true if a Google Earth app that can handle KML mine type is
* installed.
*
* @param context the context
*/
@@ -75,6 +78,8 @@ public class PlayTrackUtils {
* @param trackId the track id
*/
public static void playTrack(Context context, long trackId) {
AnalyticsUtils.sendPageViews(context, "/action/play");
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
Intent intent = new Intent(context, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_FILE_FORMAT, TrackFileFormat.KML.ordinal())
@@ -18,14 +18,13 @@ package com.google.android.apps.mytracks.util;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -37,19 +36,20 @@ import java.util.regex.Pattern;
*/
public class StringUtils {
private StringUtils() {}
private static final SimpleDateFormat ISO_8601_DATE_TIME_FORMAT = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
private static final SimpleDateFormat ISO_8601_BASE = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss");
private static final Pattern ISO_8601_EXTRAS = Pattern.compile(
"^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
/**
* Formats the date and time based on user's phone date/time preferences.
*
* @param context the context
* @param time the time in milliseconds
*/
public static String formatDateTime(Context context, long time) {
DateFormat dateFormatter = android.text.format.DateFormat.getDateFormat(context);
return dateFormatter.format(new Date(time)) + " " + formatTime(context, time);
static {
ISO_8601_DATE_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
ISO_8601_BASE.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private StringUtils() {}
/**
* Formats the time based on user's phone date/time preferences.
*
@@ -57,8 +57,28 @@ public class StringUtils {
* @param time the time in milliseconds
*/
public static String formatTime(Context context, long time) {
DateFormat timeFormatter = android.text.format.DateFormat.getTimeFormat(context);
return timeFormatter.format(new Date(time));
return android.text.format.DateFormat.getTimeFormat(context).format(time);
}
/**
* Formats the date and time based on user's phone date/time preferences.
*
* @param context the context
* @param time the time in milliseconds
*/
public static String formatDateTime(Context context, long time) {
return android.text.format.DateFormat.getDateFormat(context).format(time) + " "
+ formatTime(context, time);
}
/**
* Formats the time using the ISO 8601 date time format with fractional
* seconds in UTC time zone.
*
* @param time the time in milliseconds
*/
public static String formatDateTimeIso8601(long time) {
return ISO_8601_DATE_TIME_FORMAT.format(time);
}
/**
@@ -71,103 +91,117 @@ public class StringUtils {
}
/**
* Formats the elapsed time and total distance.
* Formats the elapsed time in the form "H:MM:SS".
*
* @param context the current context
* @param totalDistance the total distance in meters
* @param totalTime the total time in milliseconds
* @param metric whether to use metric units
* @return the formatted string
* @param time the time in milliseconds
*/
public static String formatTimeDistance(Context context, double totalDistance, long totalTime, boolean metric) {
String distanceUnit;
public static String formatElapsedTimeWithHour(long time) {
String value = formatElapsedTime(time);
return TextUtils.split(value, ":").length == 2 ? "0:" + value : value;
}
/**
* Formats the distance.
*
* @param context the context
* @param distance the distance in meters
* @param metric true to use metric. False to use imperial
*/
public static String formatDistance(Context context, double distance, boolean metric) {
if (metric) {
if (totalDistance > 2000.0) {
totalDistance *= UnitConversions.M_TO_KM;
distanceUnit = context.getString(R.string.unit_kilometer);
if (distance > 2000.0) {
distance *= UnitConversions.M_TO_KM;
return context.getString(R.string.value_float_kilometer, distance);
} else {
distanceUnit = context.getString(R.string.unit_meter);
return context.getString(R.string.value_float_meter, distance);
}
} else {
if (totalDistance * UnitConversions.M_TO_MI > 2) {
totalDistance *= UnitConversions.M_TO_MI;
distanceUnit = context.getString(R.string.unit_mile);
if (distance * UnitConversions.M_TO_MI > 2) {
distance *= UnitConversions.M_TO_MI;
return context.getString(R.string.value_float_mile, distance);
} else {
totalDistance *= UnitConversions.M_TO_FT;
distanceUnit = context.getString(R.string.unit_feet);
distance *= UnitConversions.M_TO_FT;
return context.getString(R.string.value_float_feet, distance);
}
}
return String.format("%s %.2f %s",
formatElapsedTime(totalTime),
totalDistance,
distanceUnit);
}
private static final NumberFormat SINGLE_DECIMAL_PLACE_FORMAT = NumberFormat.getNumberInstance();
static {
SINGLE_DECIMAL_PLACE_FORMAT.setMaximumFractionDigits(1);
SINGLE_DECIMAL_PLACE_FORMAT.setMinimumFractionDigits(1);
}
/**
* Formats a double precision number as decimal number with a single decimal
* place.
*
* @param number A double precision number
* @return A string representation of a decimal number, derived from the input
* double, with a single decimal place
* Formats the speed.
*
* @param context the context
* @param speed the speed in meters per second
* @param metric true to use metric. False to use imperial
* @param reportSpeed true to report as speed. False to report as pace
*/
public static final String formatSingleDecimalPlace(double number) {
return SINGLE_DECIMAL_PLACE_FORMAT.format(number);
}
/**
* Formats the given text as a CDATA element to be used in a XML file. This
* includes adding the starting and ending CDATA tags. Please notice that this
* may result in multiple consecutive CDATA tags.
*
* @param unescaped the unescaped text to be formatted
* @return the formatted text, inside one or more CDATA tags
*/
public static String stringAsCData(String unescaped) {
// "]]>" needs to be broken into multiple CDATA segments, like:
// "Foo]]>Bar" becomes "<![CDATA[Foo]]]]><![CDATA[>Bar]]>"
// (the end of the first CDATA has the "]]", the other has ">")
String escaped = unescaped.replaceAll("]]>", "]]]]><![CDATA[>");
return "<![CDATA[" + escaped + "]]>";
}
private static final SimpleDateFormat BASE_XML_DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
static {
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
}
private static final Pattern XML_DATE_EXTRAS_PATTERN =
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
/**
* Parses an XML dateTime element as defined by the XML standard.
*
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">dateTime</a>
*/
public static long parseXmlDateTime(String xmlTime) {
// Parse the base date (fixed format)
ParsePosition position = new ParsePosition(0);
Date date = BASE_XML_DATE_FORMAT.parse(xmlTime, position);
if (date == null) {
throw new IllegalArgumentException("Invalid XML dateTime value: '" + xmlTime
+ "' (at position " + position.getErrorIndex() + ")");
public static String formatSpeed(
Context context, double speed, boolean metric, boolean reportSpeed) {
if (Double.isNaN(speed) || Double.isInfinite(speed)) {
return context.getString(R.string.value_unknown);
}
if (metric) {
speed = speed * UnitConversions.MS_TO_KMH;
if (reportSpeed) {
return context.getString(R.string.value_float_kilometer_hour, speed);
} else {
double paceInMinute = speed == 0 ? 0.0 : 60 / speed;
return context.getString(R.string.value_float_minute_kilometer, paceInMinute);
}
} else {
speed = speed * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI;
if (reportSpeed) {
return context.getString(R.string.value_float_mile_hour, speed);
} else {
double paceInMinute = speed == 0 ? 0.0 : 60 / speed;
return context.getString(R.string.value_float_minute_mile, paceInMinute);
}
}
}
/**
* Formats the elapsed time and distance.
*
* @param context the context
* @param elapsedTime the elapsed time in milliseconds
* @param distance the distance in meters
* @param metric true to use metric. False to use imperial
*/
public static String formatTimeDistance(
Context context, long elapsedTime, double distance, boolean metric) {
return formatElapsedTime(elapsedTime) + " " + formatDistance(context, distance, metric);
}
// Parse the extras
Matcher matcher =
XML_DATE_EXTRAS_PATTERN.matcher(xmlTime.substring(position.getIndex()));
/**
* Formats the given text as a XML CDATA element. This includes adding the
* starting and ending CDATA tags. Please notice that this may result in
* multiple consecutive CDATA tags.
*
* @param text the given text
*/
public static String formatCData(String text) {
return "<![CDATA[" + text.replaceAll("]]>", "]]]]><![CDATA[>") + "]]>";
}
/**
* Gets the time, in milliseconds, from an XML date time string as defined at
* http://www.w3.org/TR/xmlschema-2/#dateTime
*
* @param xmlDateTime the XML date time string
*/
public static long getTime(String xmlDateTime) {
// Parse the date time base
ParsePosition position = new ParsePosition(0);
Date date = ISO_8601_BASE.parse(xmlDateTime, position);
if (date == null) {
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime
+ " (at position " + position.getErrorIndex() + ")"); }
// Parse the date time extras
Matcher matcher = ISO_8601_EXTRAS.matcher(xmlDateTime.substring(position.getIndex()));
if (!matcher.matches()) {
// This will match even an empty string as all groups are optional,
// so a non-match means some other garbage was there
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlTime);
// This will match even an empty string as all groups are optional. Thus a
// non-match means invalid content.
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime);
}
long time = date.getTime();
@@ -175,7 +209,7 @@ public class StringUtils {
// Account for fractional seconds
String fractional = matcher.group(1);
if (fractional != null) {
// Regex ensures fractional part is in (0,1(
// Regex ensures fractional part is in (0,1)
float fractionalSeconds = Float.parseFloat(fractional);
long fractionalMillis = (long) (fractionalSeconds * 1000.0f);
time += fractionalMillis;
@@ -193,24 +227,28 @@ public class StringUtils {
// Regex ensures values are >= 0
if (offsetHours > 14 || offsetMins > 59) {
throw new IllegalArgumentException("Bad timezone in " + xmlTime);
throw new IllegalArgumentException("Bad timezone: " + xmlDateTime);
}
long totalOffsetMillis = (offsetMins + offsetHours * 60L) * 60000L;
// Make time go back to UTC
// Convert to UTC
if (plusSign) {
time -= totalOffsetMillis;
} else {
time += totalOffsetMillis;
}
}
return time;
}
/**
* Gets the time as an array of parts.
* Gets the time as an array of three integers. Index 0 contains the number of
* seconds, index 1 contains the number of minutes, and index 2 contains the
* number of hours.
*
* @param time the time in milliseconds
* @return an array of 3 elements.
*/
public static int[] getTimeParts(long time) {
if (time < 0) {
@@ -224,10 +262,9 @@ public class StringUtils {
long seconds = time / 1000;
parts[0] = (int) (seconds % 60);
int tmp = (int) (seconds / 60);
parts[1] = tmp % 60;
parts[2] = tmp / 60;
int minutes = (int) (seconds / 60);
parts[1] = minutes % 60;
parts[2] = minutes / 60;
return parts;
}
}
@@ -26,7 +26,6 @@ import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.services.ControlRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.PendingIntent;
@@ -68,9 +67,6 @@ public class TrackWidgetProvider
private MyTracksProviderUtils providerUtils;
private Context context;
private String unknown;
private String distanceLabel;
private String speedLabel;
private String paceLabel;
private TrackObserver trackObserver;
private boolean isMetric;
private boolean reportSpeed;
@@ -213,32 +209,10 @@ public class TrackWidgetProvider
}
TripStatistics stats = track.getStatistics();
// TODO replace this with format strings and miles.
// convert meters to kilometers
double displayDistance = stats.getTotalDistance() * UnitConversions.M_TO_KM;
if (!isMetric) {
displayDistance *= UnitConversions.KM_TO_MI;
}
String distance =
StringUtils.formatSingleDecimalPlace(displayDistance) + " " + this.distanceLabel;
// convert ms to minutes
String distance = StringUtils.formatDistance(context, stats.getTotalDistance(), isMetric);
String time = StringUtils.formatElapsedTime(stats.getMovingTime());
String speed = unknown;
if (!Double.isNaN(stats.getAverageMovingSpeed())) {
// Convert m/s to km/h
double displaySpeed = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (!isMetric) {
displaySpeed *= UnitConversions.KM_TO_MI;
}
if (reportSpeed) {
speed = StringUtils.formatSingleDecimalPlace(displaySpeed) + " " + this.speedLabel;
} else {
long displayPace = (long) (3600000.0 / displaySpeed);
speed = StringUtils.formatElapsedTime(displayPace) + " " + paceLabel;
}
}
String speed = StringUtils.formatSpeed(
context, stats.getAverageMovingSpeed(), isMetric, reportSpeed);
views.setTextViewText(R.id.appwidget_distance_text, distance);
views.setTextViewText(R.id.appwidget_time_text, time);
@@ -250,11 +224,6 @@ public class TrackWidgetProvider
String metricUnitsKey = context.getString(R.string.metric_units_key);
if (key == null || key.equals(metricUnitsKey)) {
isMetric = prefs.getBoolean(metricUnitsKey, true);
distanceLabel = context.getString(isMetric ? R.string.unit_kilometer : R.string.unit_mile);
speedLabel = context.getString(
isMetric ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
paceLabel = context.getString(
isMetric ? R.string.unit_minute_per_kilometer : R.string.unit_minute_per_mile);
}
String reportSpeedKey = context.getString(R.string.report_speed_key);