diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 3ec314cbf..91f13973e 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -507,6 +507,34 @@ limitations under the License. %2$d elevation in feet"> Elevation gain: %1$d m (%2$d ft) + + Accuracy (m) + + + Altitude (m) + + + Bearing (deg) + + + Latitude (deg) + + + Longitude (deg) + + + Speed (m/s) + + + Time + Power (W) + + Point + + + Segment + Total time: %1$s - + Always - - %1$.1f mi + %1$.2f: a float number. E.g., '2.22'"> + %1$.2f ft + + + %1$.2f km + + + %1$.2f km/h + + + %1$.2f m + + + %1$.2f mi + + + %1$.2f mi/h + + + %1$.2f min/km + + + %1$.2f min/mi ") - .append(StringUtils.stringAsCData(value)) + .append(StringUtils.formatCData(value)) .append(""); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java index 6b5ecb57a..bdf07dbcf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java @@ -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.
+ * The first table contains the track info. Its columns are:
+ * "Track name","Activity type","Track description"
+ *
+ * The second table contains the markers. Its columns are:
+ * "Marker name","Marker type","Marker description","Latitude (deg)","Longitude + * (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"
+ *
+ * The thrid table contains the points. Its columns are:
+ * "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 (%)"
* * @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()); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java index 66afa09e1..0092dcb36 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java index ebdf1ce9c..c04cd20d3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java @@ -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(""); - printWriter.println("" + StringUtils.stringAsCData(track.getName()) + ""); - printWriter.println("" + StringUtils.stringAsCData(track.getDescription()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getName()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getDescription()) + ""); printWriter.println(""); } } @@ -111,8 +110,8 @@ public class GpxTrackWriter implements TrackFormatWriter { public void writeBeginTrack(Location firstLocation) { if (printWriter != null) { printWriter.println(""); - printWriter.println("" + StringUtils.stringAsCData(track.getName()) + ""); - printWriter.println("" + StringUtils.stringAsCData(track.getDescription()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getName()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getDescription()) + ""); printWriter.println("c0c0c0"); } } @@ -139,8 +138,7 @@ public class GpxTrackWriter implements TrackFormatWriter { if (printWriter != null) { printWriter.println(""); printWriter.println("" + ELEVATION_FORMAT.format(location.getAltitude()) + ""); - printWriter.println( - ""); + printWriter.println(""); printWriter.println(""); } } @@ -162,11 +160,10 @@ public class GpxTrackWriter implements TrackFormatWriter { if (location != null) { printWriter.println(""); printWriter.println("" + ELEVATION_FORMAT.format(location.getAltitude()) + ""); + printWriter.println(""); + printWriter.println("" + StringUtils.formatCData(waypoint.getName()) + ""); printWriter.println( - ""); - printWriter.println("" + StringUtils.stringAsCData(waypoint.getName()) + ""); - printWriter.println( - "" + StringUtils.stringAsCData(waypoint.getDescription()) + ""); + "" + StringUtils.formatCData(waypoint.getDescription()) + ""); printWriter.println(""); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java index 019c23dc8..1b8cfcb8e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java @@ -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("1"); printWriter.println("1"); printWriter.println( - "" + StringUtils.stringAsCData(track.getDescription()) + ""); - printWriter.println("" + StringUtils.stringAsCData(track.getName()) + ""); - printWriter.println("" + StringUtils.stringAsCData( + "" + StringUtils.formatCData(track.getDescription()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getName()) + ""); + printWriter.println("" + StringUtils.formatCData( context.getString(R.string.send_google_by_my_tracks, "", "")) + ""); 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(""); 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( - "" + StringUtils.stringAsCData(context.getString(R.string.menu_markers)) + "" + StringUtils.formatCData(context.getString(R.string.menu_markers)) + ""); } } @@ -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(""); + printWriter.println(""); printWriter.println( - "" + StringUtils.stringAsCData(track.getDescription()) + ""); - printWriter.println("" + StringUtils.stringAsCData(track.getName()) + ""); + "" + StringUtils.formatCData(track.getDescription()) + ""); + printWriter.println("" + StringUtils.formatCData(track.getName()) + ""); printWriter.println("#" + TRACK_STYLE + ""); printWriter.println(""); printWriter.println("absolute"); @@ -232,8 +243,7 @@ public class KmlTrackWriter implements TrackFormatWriter { @Override public void writeLocation(Location location) { if (printWriter != null) { - printWriter.println( - "" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + ""); + printWriter.println("" + StringUtils.formatDateTimeIso8601(location.getTime()) + ""); printWriter.println( "" + location.getLongitude() + " " + location.getLatitude() + " " + location.getAltitude() + ""); @@ -309,8 +319,8 @@ public class KmlTrackWriter implements TrackFormatWriter { if (location != null) { printWriter.println(""); printWriter.println( - "" + StringUtils.stringAsCData(description) + ""); - printWriter.println("" + StringUtils.stringAsCData(name) + ""); + "" + StringUtils.formatCData(description) + ""); + printWriter.println("" + StringUtils.formatCData(name) + ""); printWriter.println("#" + styleName + ""); printWriter.println(""); printWriter.println( @@ -325,9 +335,13 @@ public class KmlTrackWriter implements TrackFormatWriter { * Writes the track style. */ private void writeTrackStyle() { - printWriter.println(""); + printWriter.println(""); } /** @@ -356,7 +370,7 @@ public class KmlTrackWriter implements TrackFormatWriter { private void writeSensorStyle(String name, String displayName) { printWriter.println(""); printWriter.println( - "" + StringUtils.stringAsCData(displayName) + ""); + "" + StringUtils.formatCData(displayName) + ""); printWriter.println(""); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java index e3a3218f3..7af0eef46 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java @@ -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); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index 7085a8f79..f1b0ca8d6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -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.
+ * 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(""); + printWriter.println(""); + } + } + + @Override + public void writeFooter() { + if (printWriter != null) { + printWriter.println(""); + printWriter.println("" + + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + + ""); + // , , and are required by type=Application_t. + printWriter.println(""); + writeVersion(); + printWriter.println(""); + printWriter.println("" + Locale.getDefault().getLanguage() + ""); + printWriter.println("000-00000-00"); + printWriter.println(""); + printWriter.println(""); } - pw.format("\n", - Charset.defaultCharset().name()); - pw.print(""); - 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(" "); - pw.format(" \n", categoryToTcxSport(track.getCategory())); - pw.format(" %s\n", startTime); - pw.format(" \n", startTime); - pw.print(" "); - pw.print(track.getStatistics().getTotalTime() / 1000); - pw.println(""); - pw.print(" "); - pw.print(track.getStatistics().getTotalDistance()); - pw.println(""); - // TODO max speed etc. - // Calories are a required element just put in 0. - pw.print("0"); - pw.println("Active"); - pw.println("Manual"); - } - - @Override - public void writeOpenSegment() { - if (pw != null) { - pw.println(" "); - } - } - - @Override - public void writeLocation(Location location) { - if (pw == null) { - return; - } - pw.println(" "); - Date d = new Date(location.getTime()); - pw.println(" "); - pw.println(" "); - - pw.print(" "); - pw.print(location.getLatitude()); - pw.println(""); - - pw.print(" "); - pw.print(location.getLongitude()); - pw.println(""); - - pw.println(" "); - pw.print(" "); - pw.print(location.getAltitude()); - pw.println(""); - - 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(" "); - pw.print(""); - pw.print(sensorData.getHeartRate().getValue()); - pw.print(""); - pw.println(""); - } - - boolean cadenceAvailable = sensorData.hasCadence() - && sensorData.getCadence().getState() == Sensor.SensorState.SENDING - && sensorData.getCadence().hasValue(); - - // TCX Trackpoint_t contains a sequence. Thus, the legacy XML element - // needs to be put before . - // 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 as . - if (sportIsCycling && cadenceAvailable) { - pw.print(" "); - pw.print(Math.min(254, sensorData.getCadence().getValue())); - pw.println(""); - } - - boolean powerAvailable = sensorData.hasPower() - && sensorData.getPower().getState() == Sensor.SensorState.SENDING - && sensorData.getPower().hasValue(); - - if(powerAvailable || (!sportIsCycling && cadenceAvailable)) { - pw.print(" "); - pw.print(""); - - // RunCadence needs to be put before power in order to be understood - // by Garmin Training Center. - if (!sportIsCycling && cadenceAvailable) { - pw.print(""); - pw.print(Math.min(254, sensorData.getCadence().getValue())); - pw.print(""); - } - - if (powerAvailable) { - pw.print(""); - pw.print(sensorData.getPower().getValue()); - pw.print(""); - } - - pw.println(""); - } - } - } - pw.println(" "); - } - - @Override - public void writeCloseSegment() { - if (pw != null) { - pw.println(" "); + printWriter.println(""); + printWriter.println(""); + printWriter.println("" + startTime + ""); + printWriter.println(""); + printWriter.println("" + totalTimeInSeconds + ""); + printWriter.println("" + track.getStatistics().getTotalDistance() + + ""); + // is required, just put in 0. + printWriter.println("0"); + printWriter.println("Active"); + printWriter.println("Manual"); } } @Override public void writeEndTrack(Location lastPoint) { - if (pw == null) { - return; + if (printWriter != null) { + printWriter.println(""); + printWriter.println(""); + printWriter.println("" + + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + + ""); + // , , and are required for type=Device_t. + printWriter.println("0"); + printWriter.println("0"); + writeVersion(); + printWriter.println(""); + printWriter.println(""); + printWriter.println(""); } - pw.println(" "); - pw.print(" "); - pw.format("My Tracks running on %s\n", Build.MODEL); - - // The following code is correct. ID is inconsistently capitalized in the - // TCX schema. - pw.println("0"); - pw.println("0"); - - writeVersion(); - - pw.println(""); - pw.println(" "); - pw.println(" "); } @Override - public void writeFooter() { - if (pw == null) { - return; + public void writeOpenSegment() { + if (printWriter != null) { + printWriter.println(""); } - pw.println(" "); + } - // 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("My Tracks %s by Google\n", SystemUtils.getMyTracksVersion(context)); + @Override + public void writeCloseSegment() { + if (printWriter != null) { + printWriter.println(""); + } + } - pw.println(""); + @Override + public void writeLocation(Location location) { + if (printWriter != null) { + printWriter.println(""); + printWriter.println(""); + printWriter.println(""); + printWriter.println("" + location.getLatitude() + ""); + printWriter.println("" + location.getLongitude() + ""); + printWriter.println(""); + printWriter.println("" + location.getAltitude() + ""); - 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(""); + printWriter.println("" + sensorDataSet.getHeartRate().getValue() + ""); + printWriter.println(""); + } - pw.format("%s\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE - : TCX_TYPE_INTERNAL); - pw.println(""); - pw.format("%s\n", Locale.getDefault().getLanguage()); - pw.println("000-00000-00"); - pw.println(""); - pw.println(""); + // needs to be put before . + // According to the TCX spec, is only for the biking sport + // type. For others, use in . + if (cadenceAvailable && sportType == SportType.BIKING) { + // The spec requires the max value be 254. + printWriter.println( + "" + Math.min(254, sensorDataSet.getCadence().getValue()) + ""); + } + + if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) { + printWriter.println(""); + printWriter.println( + ""); + + // needs to be put before . + if (cadenceAvailable && sportType != SportType.BIKING) { + // The spec requires the max value to be 254. + printWriter.println("" + + Math.min(254, sensorDataSet.getCadence().getValue()) + ""); + } + + if (powerAvailable) { + printWriter.println("" + sensorDataSet.getPower().getValue() + ""); + } + printWriter.println(""); + printWriter.println(""); + } + } + } + printWriter.println(""); + } } @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(""); - pw.format("%d\n", Integer.valueOf(myTracksVersionComponents[0])); - pw.format("%d\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("%d\n", Integer.valueOf(myTracksVersionComponents[2])); - pw.format("%d\n", SystemUtils.isRelease(context) ? 0 : 1); - pw.println(""); + printWriter.println(""); + printWriter.println("" + versionMajor + ""); + printWriter.println("" + versionMinor + ""); + // According to TCX spec, these are optional. But http://connect.garmin.com + // requires them. + printWriter.println("" + buildMajor + ""); + printWriter.println("0"); + printWriter.println(""); } - 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; } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java index 5af558ae0..7df87364f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java @@ -50,7 +50,7 @@ public class TrackWriterFactory { CSV { @Override public TrackFormatWriter newFormatWriter(Context context) { - return new CsvTrackWriter(); + return new CsvTrackWriter(context); } }, TCX { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java index 052561d90..92bac1af4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java @@ -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; + } + } + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index eb91bc30a..b8600dcdd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java index 25871fad2..8ba5f3de4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java @@ -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 arrayAdapter; + @VisibleForTesting + ArrayAdapter arrayAdapter; @Override public void onCreate(Bundle savedInstanceState) { @@ -166,7 +169,7 @@ public class ChooseMapActivity extends Activity { */ public void onAsyncTaskCompleted( boolean success, ArrayList mapIds, ArrayList 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; } } -} +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java index 9d54efed5..c500c96f6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java @@ -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; + } + } + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java index f18a3c164..00c866922 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -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); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java index 3de566876..626cf3177 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java @@ -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); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java index 5e4859c85..5a18b927b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java @@ -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 pages = new ArrayList(); 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 diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/AnalyticsUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/AnalyticsUtils.java new file mode 100644 index 000000000..d0b00dea3 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/AnalyticsUtils.java @@ -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(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java index caca3ff32..99ce45838 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -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. * diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/PlayTrackUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/PlayTrackUtils.java index b890de426..09e37656d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/PlayTrackUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/PlayTrackUtils.java @@ -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()) diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java index b77f737c5..c7980a1fb 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java @@ -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 "Bar]]>" - // (the end of the first CDATA has the "]]", the other has ">") - String escaped = unescaped.replaceAll("]]>", "]]]]>"); - return ""; - } - - 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 dateTime - */ - 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 "", "]]]]>") + "]]>"; + } + + /** + * 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; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/widgets/TrackWidgetProvider.java b/MyTracks/src/com/google/android/apps/mytracks/widgets/TrackWidgetProvider.java index 49ab1df9c..9d123295d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/widgets/TrackWidgetProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/widgets/TrackWidgetProvider.java @@ -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); diff --git a/MyTracksTest/.classpath b/MyTracksTest/.classpath index d91442653..6044ff11f 100644 --- a/MyTracksTest/.classpath +++ b/MyTracksTest/.classpath @@ -10,6 +10,6 @@ - + diff --git a/MyTracksTest/libs/robotium-solo-2.1.jar b/MyTracksTest/libs/robotium-solo-2.1.jar deleted file mode 100644 index 73af311f6..000000000 Binary files a/MyTracksTest/libs/robotium-solo-2.1.jar and /dev/null differ diff --git a/MyTracksTest/libs/robotium-solo-3.1.jar b/MyTracksTest/libs/robotium-solo-3.1.jar new file mode 100644 index 000000000..0ba05859b Binary files /dev/null and b/MyTracksTest/libs/robotium-solo-3.1.jar differ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java index 966bcd470..ffd516c4b 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartActivityTest.java @@ -43,13 +43,6 @@ public class ChartActivityTest extends ActivityInstrumentationTestCase2 segmentTags = getChildElements(trackTag, "trkseg", 2); List segPointTags = getChildElements(segmentTags.get(0), "trkpt", 2); - assertTagMatchesLocation(segPointTags.get(0), "0", "0", "1970-01-01T02:46:40Z", "0"); - assertTagMatchesLocation(segPointTags.get(1), "1", "-1", "1970-01-01T02:46:41Z", "5000000"); + assertTagMatchesLocation(segPointTags.get(0), "0", "0", "1970-01-01T00:00:00Z", "0"); + assertTagMatchesLocation(segPointTags.get(1), "1", "-1", "1970-01-01T00:01:40Z", "10"); segPointTags = getChildElements(segmentTags.get(1), "trkpt", 2); - assertTagMatchesLocation(segPointTags.get(0), "2", "-2", "1970-01-01T02:46:42Z", "10000000"); - assertTagMatchesLocation(segPointTags.get(1), "3", "-3", "1970-01-01T02:46:43Z", "15000000"); + assertTagMatchesLocation(segPointTags.get(0), "2", "-2", "1970-01-01T00:03:20Z", "20"); + assertTagMatchesLocation(segPointTags.get(1), "3", "-3", "1970-01-01T00:05:00Z", "30"); List waypointTags = getChildElements(gpxTag, "wpt", 2); Element wptTag = waypointTags.get(0); assertEquals(WAYPOINT1_NAME, getChildTextValue(wptTag, "name")); assertEquals(WAYPOINT1_DESCRIPTION, getChildTextValue(wptTag, "desc")); - assertTagMatchesLocation(wptTag, "1", "-1", "1970-01-01T02:46:41Z", "5000000"); + assertTagMatchesLocation(wptTag, "1", "-1", "1970-01-01T00:01:40Z", "10"); wptTag = waypointTags.get(1); assertEquals(WAYPOINT2_NAME, getChildTextValue(wptTag, "name")); assertEquals(WAYPOINT2_DESCRIPTION, getChildTextValue(wptTag, "desc")); - assertTagMatchesLocation(wptTag, "2", "-2", "1970-01-01T02:46:42Z", "10000000"); + assertTagMatchesLocation(wptTag, "2", "-2", "1970-01-01T00:03:20Z", "20"); } /** diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java index 9d23eb5e0..93c5a9aa2 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java @@ -63,14 +63,19 @@ public class KmlTrackWriterTest extends TrackFormatWriterTest { assertEquals(TRACK_NAME, getChildTextValue(docTag, "name")); assertEquals(TRACK_DESCRIPTION, getChildTextValue(docTag, "description")); - // There are 5 placemarks - start, segments, end, waypoint1, waypoint2 - List placemarkTags = getChildElements(docTag, "Placemark", 5); + // There are 3 placemarks - start, track, and end + List placemarkTags = getChildElements(docTag, "Placemark", 3); assertTagIsPlacemark( placemarkTags.get(0), TRACK_NAME + " (Start)", TRACK_DESCRIPTION, location1); assertTagIsPlacemark( placemarkTags.get(2), TRACK_NAME + " (End)", FULL_TRACK_DESCRIPTION, location4); - assertTagIsPlacemark(placemarkTags.get(3), WAYPOINT1_NAME, WAYPOINT1_DESCRIPTION, location2); - assertTagIsPlacemark(placemarkTags.get(4), WAYPOINT2_NAME, WAYPOINT2_DESCRIPTION, location3); + + List folderTag = getChildElements(docTag, "Folder", 1); + List folderPlacemarkTags = getChildElements(folderTag.get(0), "Placemark", 2); + assertTagIsPlacemark( + folderPlacemarkTags.get(0), WAYPOINT1_NAME, WAYPOINT1_DESCRIPTION, location2); + assertTagIsPlacemark( + folderPlacemarkTags.get(1), WAYPOINT2_NAME, WAYPOINT2_DESCRIPTION, location3); Element trackPlacemarkTag = placemarkTags.get(1); assertEquals(TRACK_NAME, getChildTextValue(trackPlacemarkTag, "name")); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java index 6594cf51c..547229a4e 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java @@ -1,9 +1,23 @@ -// Copyright 2010 Google Inc. All Rights Reserved. +/* + * Copyright 2010 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.io.file; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.util.FileUtils; +import com.google.android.apps.mytracks.util.StringUtils; import java.util.List; @@ -11,7 +25,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; /** - * Tests for the GPX track exporter. + * Tests for {@link TcxTrackWriter}. * * @author Sandor Dornbush */ @@ -37,42 +51,46 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { } /** - * Asserts that the given tags describe the given points, in the same order. + * Asserts that the given tags describe the given locations in the same order. + * + * @param tags list of tags + * @param locations list of locations */ - private void assertTagsMatchPoints(List tags, MyTracksLocation... locs) { - assertEquals(locs.length, tags.size()); - for (int i = 0; i < locs.length; i++) { - Element tag = tags.get(i); - MyTracksLocation loc = locs[i]; - - assertTagMatchesLocation(tag, loc); + private void assertTagsMatchPoints(List tags, MyTracksLocation... locations) { + assertEquals(locations.length, tags.size()); + for (int i = 0; i < locations.length; i++) { + assertTagMatchesLocation(tags.get(i), locations[i]); } } /** * Asserts that the given tag describes the given location. + * + * @param tag the tag + * @param location the location */ - private void assertTagMatchesLocation(Element tag, MyTracksLocation loc) { - Element posTag = getChildElement(tag, "Position"); - assertEquals(Double.toString(loc.getLatitude()), - getChildTextValue(posTag, "LatitudeDegrees")); - assertEquals(Double.toString(loc.getLongitude()), - getChildTextValue(posTag, "LongitudeDegrees")); + private void assertTagMatchesLocation(Element tag, MyTracksLocation location) { + assertEquals(StringUtils.formatDateTimeIso8601(location.getTime()), getChildTextValue(tag, "Time")); - assertEquals(FileUtils.FILE_TIMESTAMP_FORMAT.format(loc.getTime()), - getChildTextValue(tag, "Time")); - assertEquals(Double.toString(loc.getAltitude()), - getChildTextValue(tag, "AltitudeMeters")); - assertTrue(loc.getSensorDataSet() != null); - Sensor.SensorDataSet sds = loc.getSensorDataSet(); + Element positionTag = getChildElement(tag, "Position"); + assertEquals( + Double.toString(location.getLatitude()), getChildTextValue(positionTag, "LatitudeDegrees")); + assertEquals(Double.toString(location.getLongitude()), + getChildTextValue(positionTag, "LongitudeDegrees")); - List bpm = getChildElements(tag, "HeartRateBpm", 1); + assertEquals(Double.toString(location.getAltitude()), getChildTextValue(tag, "AltitudeMeters")); + assertTrue(location.getSensorDataSet() != null); + Sensor.SensorDataSet sds = location.getSensorDataSet(); + + List heartRate = getChildElements(tag, "HeartRateBpm", 1); assertEquals(Integer.toString(sds.getHeartRate().getValue()), - getChildTextValue(bpm.get(0), "Value")); + getChildTextValue(heartRate.get(0), "Value")); - List ext = getChildElements(tag, "Extensions", 1); - List tpx = getChildElements(ext.get(0), "TPX", 1); - assertEquals(Integer.toString(sds.getPower().getValue()), - getChildTextValue(tpx.get(0), "Watts")); + List extensions = getChildElements(tag, "Extensions", 1); + List tpx = getChildElements(extensions.get(0), "TPX", 1); + assertEquals( + Integer.toString(sds.getCadence().getValue()), getChildTextValue(tpx.get(0), "RunCadence")); + assertEquals( + Integer.toString(sds.getPower().getValue()), getChildTextValue(tpx.get(0), "Watts")); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java index bba7f8df7..4afb7465e 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java @@ -39,10 +39,13 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { // All the user-provided strings have "]]>" to ensure that proper escaping is // being done. protected static final String TRACK_NAME = "Home]]>"; + protected static final String TRACK_CATEGORY = "Hiking"; protected static final String TRACK_DESCRIPTION = "The long ]]> journey home"; protected static final String WAYPOINT1_NAME = "point]]>1"; + protected static final String WAYPOINT1_CATEGORY = "Statistics"; protected static final String WAYPOINT1_DESCRIPTION = "point 1]]>description"; protected static final String WAYPOINT2_NAME = "point]]>2"; + protected static final String WAYPOINT2_CATEGORY = "Waypoint"; protected static final String WAYPOINT2_DESCRIPTION = "point 2]]>description"; private static final int BUFFER_SIZE = 10240; protected Track track; @@ -55,6 +58,7 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { track = new Track(); track.setName(TRACK_NAME); + track.setCategory(TRACK_CATEGORY); track.setDescription(TRACK_DESCRIPTION); location1 = new MyTracksLocation("mock"); @@ -67,33 +71,40 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { wp2 = new Waypoint(); wp1.setLocation(location2); wp1.setName(WAYPOINT1_NAME); + wp1.setCategory(WAYPOINT1_CATEGORY); wp1.setDescription(WAYPOINT1_DESCRIPTION); wp2.setLocation(location3); wp2.setName(WAYPOINT2_NAME); + wp2.setCategory(WAYPOINT2_CATEGORY); wp2.setDescription(WAYPOINT2_DESCRIPTION); } /** - * Populates the given locations with coordinates and time. + * Populates a list of locations with values. + * + * @param locations a list of locations */ - protected void populateLocations(MyTracksLocation... locs) { - for (int i = 0; i < locs.length; i++) { - MyTracksLocation loc = locs[i]; - loc.setAltitude(i * 5000000); - loc.setLatitude(i); - loc.setLongitude(-i); - loc.setTime(10000000 + i * 1000); - Sensor.SensorData.Builder hr = Sensor.SensorData.newBuilder() - .setValue(100 + i) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder() - .setValue(400 + i) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorDataSet sds = - Sensor.SensorDataSet.newBuilder().setHeartRate(hr.build()) - .setPower(power) - .build(); - loc.setSensorData(sds); + private void populateLocations(MyTracksLocation... locations) { + for (int i = 0; i < locations.length; i++) { + MyTracksLocation location = locations[i]; + location.setLatitude(i); + location.setLongitude(-i); + location.setAltitude(i * 10); + location.setBearing(i * 100); + location.setAccuracy(i * 1000); + location.setSpeed(i * 10000); + location.setTime(i * 100000); + Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder().setValue(100 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder().setValue(200 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder heartRate = Sensor.SensorData.newBuilder().setValue(300 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder().setValue(400 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorDataSet sensorDataSet = Sensor.SensorDataSet.newBuilder().setPower(power) + .setCadence(cadence).setHeartRate(heartRate).setBatteryLevel(batteryLevel).build(); + location.setSensorData(sensorDataSet); } } @@ -108,6 +119,10 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { OutputStream output = new ByteArrayOutputStream(BUFFER_SIZE); writer.prepare(track, output); writer.writeHeader(); + writer.writeBeginWaypoints(); + writer.writeWaypoint(wp1); + writer.writeWaypoint(wp2); + writer.writeEndWaypoints(); writer.writeBeginTrack(location1); writer.writeOpenSegment(); writer.writeLocation(location1); @@ -118,8 +133,6 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { writer.writeLocation(location4); writer.writeCloseSegment(); writer.writeEndTrack(location4); - writer.writeWaypoint(wp1); - writer.writeWaypoint(wp2); writer.writeFooter(); writer.close(); return output.toString(); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivityTest.java new file mode 100644 index 000000000..09b095a5f --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivityTest.java @@ -0,0 +1,85 @@ +/* + * 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.io.fusiontables; + +import com.google.android.apps.mytracks.io.docs.SendDocsActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity; + +import android.test.AndroidTestCase; + +/** + * Tests the {@link SendFusionTablesActivity}. + * + * @author Youtao Liu + */ +public class SendFusionTablesActivityTest extends AndroidTestCase { + + private SendFusionTablesActivity sendFusionTablesActivity; + private SendRequest sendRequest; + + @Override + protected void setUp() throws Exception { + super.setUp(); + sendRequest = new SendRequest(1L, true, false, true); + sendFusionTablesActivity = new SendFusionTablesActivity(); + } + + /** + * Tests the method + * {@link SendFusionTablesActivity#getNextClass(SendRequest, boolean)}. Sets + * the flags of "sendDocs" and "cancel" to false and false. + */ + public void testGetNextClass_notCancelNotSendDocs() { + sendRequest.setSendDocs(false); + Class next = sendFusionTablesActivity.getNextClass(sendRequest, false); + assertEquals(UploadResultActivity.class, next); + } + + /** + * Tests the method + * {@link SendFusionTablesActivity#getNextClass(SendRequest, boolean)}. Sets + * the flags of "sendDocs" and "cancel" to true and false. + */ + public void testGetNextClass_notCancelSendDocs() { + sendRequest.setSendDocs(true); + Class next = sendFusionTablesActivity.getNextClass(sendRequest, false); + assertEquals(SendDocsActivity.class, next); + } + + /** + * Tests the method + * {@link SendFusionTablesActivity#getNextClass(SendRequest,boolean)}. Sets + * the flags of "sendDocs" and "cancel" to true and true. + */ + public void testGetNextClass_cancelSendDocs() { + sendRequest.setSendDocs(true); + Class next = sendFusionTablesActivity.getNextClass(sendRequest, true); + assertEquals(UploadResultActivity.class, next); + } + + /** + * Tests the method + * {@link SendFusionTablesActivity#getNextClass(SendRequest,boolean)}. Sets + * the flags of "sendDocs" and "cancel" to false and true. + */ + public void testGetNextClass_cancelNotSendDocs() { + sendRequest.setSendDocs(false); + Class next = sendFusionTablesActivity.getNextClass(sendRequest, true); + assertEquals(UploadResultActivity.class, next); + } + +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivityTest.java new file mode 100644 index 000000000..8c6ba5d13 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivityTest.java @@ -0,0 +1,144 @@ +/* + * 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.io.maps; + +import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata; +import com.google.android.maps.mytracks.R; + +import android.content.Intent; +import android.test.AndroidTestCase; +import android.widget.ArrayAdapter; + +import java.util.ArrayList; + +/** + * Tests the {@link ChooseMapActivity}. + * + * @author Youtao Liu + */ +public class ChooseMapActivityTest extends AndroidTestCase { + + private static final String MAP_ID = "mapid"; + private static final String MAP_TITLE = "title"; + private static final String MAP_DESC = "desc"; + + private ArrayList mapIds = new ArrayList(); + private ArrayList mapDatas = new ArrayList(); + private boolean errorDialogShown = false; + private boolean progressDialogRemoved = false; + + /** + * Creates a class to override some methods of {@link ChooseMapActivity} to + * makes it testable. + * + * @author youtaol + */ + public class ChooseMapActivityMock extends ChooseMapActivity { + /** + * By overriding this method, avoids to start next activity. + */ + @Override + public void startActivity(Intent intent) {} + + /** + * By overriding this method, avoids to show an error dialog and set the + * show flag to true. + */ + @Override + public void showErrorDialog() { + errorDialogShown = true; + } + + /** + * By overriding this method, avoids to show an error dialog and set the + * show flag to true. + */ + @Override + public void removeProgressDialog() { + progressDialogRemoved = true; + } + } + + /** + * Tests the method + * {@link ChooseMapActivity#onAsyncTaskCompleted(boolean, ArrayList, ArrayList)} + * . An alert dialog should be shown when there is no map. + */ + public void testOnAsyncTaskCompleted_fail() { + ChooseMapActivityMock chooseMapActivityMock = new ChooseMapActivityMock(); + errorDialogShown = false; + progressDialogRemoved = false; + chooseMapActivityMock.onAsyncTaskCompleted(false, null, null); + assertTrue(progressDialogRemoved); + assertTrue(errorDialogShown); + } + + /** + * Tests the method + * {@link ChooseMapActivity#onAsyncTaskCompleted(boolean, ArrayList, ArrayList)} + * . Check the logic when there is only map. + */ + public void testOnAsyncTaskCompleted_success_oneMap() { + ChooseMapActivityMock chooseMapActivityMock = new ChooseMapActivityMock(); + chooseMapActivityMock.arrayAdapter = new ArrayAdapter(getContext(), + R.layout.choose_map_item); + simulateMaps(1); + chooseMapActivityMock.onAsyncTaskCompleted(true, mapIds, mapDatas); + assertEquals(1, chooseMapActivityMock.arrayAdapter.getCount()); + assertEquals(MAP_ID + "0", chooseMapActivityMock.arrayAdapter.getItem(0).getMapId()); + assertEquals(MAP_TITLE + "0", chooseMapActivityMock.arrayAdapter.getItem(0).getMapData() + .getTitle()); + assertEquals(MAP_DESC + "0", chooseMapActivityMock.arrayAdapter.getItem(0).getMapData() + .getDescription()); + } + + /** + * Tests the method + * {@link ChooseMapActivity#onAsyncTaskCompleted(boolean, ArrayList, ArrayList)} + * . Check the logic when there are 10 maps. + */ + public void testOnAsyncTaskCompleted_success_twoMaps() { + ChooseMapActivityMock chooseMapActivityMock = new ChooseMapActivityMock(); + chooseMapActivityMock.arrayAdapter = new ArrayAdapter(getContext(), + R.layout.choose_map_item); + simulateMaps(10); + chooseMapActivityMock.onAsyncTaskCompleted(true, mapIds, mapDatas); + assertEquals(10, chooseMapActivityMock.arrayAdapter.getCount()); + assertEquals(MAP_ID + "9", chooseMapActivityMock.arrayAdapter.getItem(9).getMapId()); + assertEquals(MAP_TITLE + "9", chooseMapActivityMock.arrayAdapter.getItem(9).getMapData() + .getTitle()); + assertEquals(MAP_DESC + "9", chooseMapActivityMock.arrayAdapter.getItem(9).getMapData() + .getDescription()); + } + + /** + * Simulates map data for the test. + * + * @param number of data should be simulated. + */ + private void simulateMaps(int number) { + mapIds = new ArrayList(); + mapDatas = new ArrayList(); + for (int i = 0; i < number; i++) { + mapIds.add(MAP_ID + i); + MapsMapMetadata metaData = new MapsMapMetadata(); + metaData.setTitle(MAP_TITLE + i); + metaData.setDescription(MAP_DESC + i); + metaData.setSearchable(true); + mapDatas.add(metaData); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsActivityTest.java new file mode 100644 index 000000000..d2646609a --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsActivityTest.java @@ -0,0 +1,90 @@ +/* + * 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.io.maps; + +import com.google.android.apps.mytracks.io.docs.SendDocsActivity; +import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity; + +import android.test.AndroidTestCase; + +/** + * Tests the {@link SendMapsActivity}. + * + * @author Youtao Liu + */ +public class SendMapsActivityTest extends AndroidTestCase { + + private SendMapsActivity sendMapsActivity; + private SendRequest sendRequest; + + @Override + protected void setUp() throws Exception { + super.setUp(); + sendRequest = new SendRequest(1L, true, false, true); + sendMapsActivity = new SendMapsActivity(); + } + + /** + * Tests the method + * {@link SendMapsActivity#getNextClass(SendRequest, boolean)}. Sets the flags + * of "sendFusionTables","sendDocs" and "cancel" to true, true and false. + */ + public void testGetNextClass_notCancelSendFusionTables() { + sendRequest.setSendFusionTables(true); + sendRequest.setSendDocs(true); + Class next = sendMapsActivity.getNextClass(sendRequest, false); + assertEquals(SendFusionTablesActivity.class, next); + } + + /** + * Tests the method + * {@link SendMapsActivity#getNextClass(SendRequest, boolean)}. Sets the flags + * of "sendFusionTables","sendDocs" and "cancel" to false, true and false. + */ + public void testGetNextClass_notCancelSendDocs() { + sendRequest.setSendFusionTables(false); + sendRequest.setSendDocs(true); + Class next = sendMapsActivity.getNextClass(sendRequest, false); + assertEquals(SendDocsActivity.class, next); + } + + /** + * Tests the method + * {@link SendMapsActivity#getNextClass(SendRequest, boolean)}. Sets the flags + * of "sendFusionTables","sendDocs" and "cancel" to false, false and false. + */ + public void testGetNextClass_notCancelNotSend() { + sendRequest.setSendFusionTables(false); + sendRequest.setSendDocs(false); + Class next = sendMapsActivity.getNextClass(sendRequest, false); + assertEquals(UploadResultActivity.class, next); + } + + /** + * Tests the method + * {@link SendMapsActivity#getNextClass(SendRequest, boolean)}. Sets the flags + * of "sendFusionTables","sendDocs" and "cancel" to true, true and true. + */ + public void testGetNextClass_cancelSendDocs() { + sendRequest.setSendFusionTables(true); + sendRequest.setSendDocs(true); + Class next = sendMapsActivity.getNextClass(sendRequest, true); + assertEquals(UploadResultActivity.class, next); + } + +} \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequestTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequestTest.java index 1a8f8a7e2..cbac258a2 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequestTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequestTest.java @@ -245,7 +245,7 @@ public class SendRequestTest extends AndroidTestCase { } /** - * Tests {@link SendRequest#writeToParcel(Parcel in)} when all input values + * Tests {@link SendRequest#writeToParcel(Parcel, int)} when all input values * are true or affirmative. */ public void testWriteToParcel_allTrue() { @@ -283,7 +283,7 @@ public class SendRequestTest extends AndroidTestCase { } /** - * Tests {@link SendRequest#writeToParcel(Parcel in)} when all input values + * Tests {@link SendRequest#writeToParcel(Parcel, int)} when all input values * are false or negative. */ public void testWriteToParcel_allFalse() { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivityTest.java index 785f08a87..de378c3e8 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivityTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivityTest.java @@ -51,7 +51,7 @@ public class UploadServiceChooserActivityTest extends /** * Tests the logic to control display all send options. This test cover code - * in method {@link UploadServiceChooserActivity#onCreateDialog()}, + * in method {@link UploadServiceChooserActivity#onCreateDialog(int)}, * {@link UploadServiceChooserActivity#updateStateBySendRequest()} and * {@link UploadServiceChooserActivity#updateStateBySelection()}. */ @@ -91,7 +91,7 @@ public class UploadServiceChooserActivityTest extends /** * Tests the logic to check the send to Google Maps option. This test cover - * code in method {@link UploadServiceChooserActivity#onCreateDialog()}, + * code in method {@link UploadServiceChooserActivity#onCreateDialog(int)}, * {@link UploadServiceChooserActivity#updateStateBySendRequest()} and * {@link UploadServiceChooserActivity#updateStateBySelection()}. */ @@ -120,7 +120,7 @@ public class UploadServiceChooserActivityTest extends /** * Tests the logic to control display none. This test cover code in method - * {@link UploadServiceChooserActivity#onCreateDialog()}, + * {@link UploadServiceChooserActivity#onCreateDialog(int)}, * {@link UploadServiceChooserActivity#updateStateBySendRequest()} and * {@link UploadServiceChooserActivity#updateStateBySelection()}. */ @@ -135,7 +135,7 @@ public class UploadServiceChooserActivityTest extends /** * Tests the logic to initial state of check box to unchecked. This test cover - * code in method {@link UploadServiceChooserActivity#onCreateDialog()}, + * code in method {@link UploadServiceChooserActivity#onCreateDialog(int)}, * {@link UploadServiceChooserActivity#initState()}. */ public void testOnCreateDialog_initStateUnchecked() { @@ -162,7 +162,7 @@ public class UploadServiceChooserActivityTest extends /** * Tests the logic to initial state of check box to checked. This test cover - * code in method {@link UploadServiceChooserActivity#onCreateDialog()}, + * code in method {@link UploadServiceChooserActivity#onCreateDialog(int)}, * {@link UploadServiceChooserActivity#initState()}. */ public void testOnCreateDialog_initStateChecked() { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/StringUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/StringUtilsTest.java index adbe6bbf4..cb698913c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/StringUtilsTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/StringUtilsTest.java @@ -15,68 +15,217 @@ */ package com.google.android.apps.mytracks.util; +import android.test.AndroidTestCase; + import java.util.GregorianCalendar; import java.util.TimeZone; -import junit.framework.TestCase; - /** * Tests for {@link StringUtils}. * * @author Rodrigo Damazio */ -public class StringUtilsTest extends TestCase { - public void testParseXmlDateTime() { - assertParseXmlDateTime("2010-05-04T03:02:01", - 2010, 5, 4, 3, 2, 1, 0); +public class StringUtilsTest extends AndroidTestCase { + + /** + * Tests {@link StringUtils#formatDateTime(android.content.Context, long)}. + */ + public void testFormatTime() { + // Unix time 0 in UTC is "4:00 PM" in Pacific Standard time zone. + // This test can break if run on a different time zone or with a different + // time format preference. + assertEquals("4:00 PM", StringUtils.formatTime(getContext(), 0L)); } - public void testParseXmlDateTime_fractional() { - assertParseXmlDateTime("2010-05-04T03:02:01.3", - 2010, 5, 4, 3, 2, 1, 300); - assertParseXmlDateTime("2010-05-04T03:02:01.35", - 2010, 5, 4, 3, 2, 1, 350); - assertParseXmlDateTime("2010-05-04T03:02:01.352", - 2010, 5, 4, 3, 2, 1, 352); - assertParseXmlDateTime("2010-05-04T03:02:01.3525", - 2010, 5, 4, 3, 2, 1, 352); + /** + * Tests {@link StringUtils#formatDateTime(android.content.Context, long)}. + */ + public void testFormatDateTime() { + // Unix time 0 in UTC is "12/31/1969 4:00 PM" in Pacific Standard time zone. + // This test can break if run on a different time zone or with a different + // date/time format preference. + assertEquals("12/31/1969 4:00 PM", StringUtils.formatDateTime(getContext(), 0L)); + } + + /** + * Tests {@link StringUtils#formatDateTimeIso8601(long)}. + */ + public void testFormatDateTimeIso8601() { + assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345)); } - public void testParseXmlDateTime_timezone() { - assertParseXmlDateTime("2010-05-04T03:02:01Z", - 2010, 5, 4, 3, 2, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01+00:00", - 2010, 5, 4, 3, 2, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01-00:00", - 2010, 5, 4, 3, 2, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01+01:00", - 2010, 5, 4, 2, 2, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01+10:30", - 2010, 5, 3, 16, 32, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01-09:30", - 2010, 5, 4, 12, 32, 1, 0); - assertParseXmlDateTime("2010-05-04T03:02:01-05:00", - 2010, 5, 4, 8, 2, 1, 0); + /** + * Tests {@link StringUtils#formatElapsedTime(long)}. + */ + public void testformatElapsedTime() { + // 1 second + assertEquals("00:01", StringUtils.formatElapsedTime(1000)); + // 10 seconds + assertEquals("00:10", StringUtils.formatElapsedTime(10000)); + // 1 minute + assertEquals("01:00", StringUtils.formatElapsedTime(60000)); + // 10 minutes + assertEquals("10:00", StringUtils.formatElapsedTime(600000)); + // 1 hour + assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000)); + // 10 hours + assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000)); + // 100 hours + assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000)); } - public void testParseXmlDateTime_fractionalAndTimezone() { - assertParseXmlDateTime("2010-05-04T03:02:01.352Z", - 2010, 5, 4, 3, 2, 1, 352); - assertParseXmlDateTime("2010-05-04T03:02:01.47+00:00", - 2010, 5, 4, 3, 2, 1, 470); - assertParseXmlDateTime("2010-05-04T03:02:01.5791+03:00", - 2010, 5, 4, 0, 2, 1, 579); - assertParseXmlDateTime("2010-05-04T03:02:01.8-05:30", - 2010, 5, 4, 8, 32, 1, 800); + /** + * Tests {@link StringUtils#formatElapsedTimeWithHour(long)}. + */ + public void testformatElapsedTimeWithHour() { + // 1 second + assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000)); + // 10 seconds + assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000)); + // 1 minute + assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000)); + // 10 minutes + assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000)); + // 1 hour + assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000)); + // 10 hours + assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000)); + // 100 hours + assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000)); } - private void assertParseXmlDateTime(String dateTime, - int year, int month, int day, int hour, int min, int second, int millis) { - long timestamp = StringUtils.parseXmlDateTime(dateTime); - GregorianCalendar calendar = - new GregorianCalendar(TimeZone.getTimeZone("UTC")); - calendar.set(year, month - 1, day, hour, min, second); - calendar.set(GregorianCalendar.MILLISECOND, millis); - assertEquals(calendar.getTimeInMillis(), timestamp); + /** + * Tests {@link StringUtils#formatDistance(android.content.Context, double, + * boolean)}. + */ + public void testFormatDistance() { + // A large number in metric + assertEquals("5.00 km", StringUtils.formatDistance(getContext(), 5000, true)); + // A large number in imperial + assertEquals("3.11 mi", StringUtils.formatDistance(getContext(), 5000, false)); + // A small number in metric + assertEquals("100.00 m", StringUtils.formatDistance(getContext(), 100, true)); + // A small number in imperial + assertEquals("328.08 ft", StringUtils.formatDistance(getContext(), 100, false)); + } + + /** + * Tests {@link StringUtils#formatSpeed(android.content.Context, double, + * boolean, boolean)}. + */ + public void testFormatSpeed() { + // Speed in metric + assertEquals("36.00 km/h", StringUtils.formatSpeed(getContext(), 10, true, true)); + // Speed in imperial + assertEquals("22.37 mi/h", StringUtils.formatSpeed(getContext(), 10, false, true)); + // Pace in metric + assertEquals("1.67 min/km", StringUtils.formatSpeed(getContext(), 10, true, false)); + // Pace in imperial + assertEquals("2.68 min/mi", StringUtils.formatSpeed(getContext(), 10, false, false)); + // zero pace + assertEquals("0.00 min/km", StringUtils.formatSpeed(getContext(), 0, true, false)); + assertEquals("0.00 min/mi", StringUtils.formatSpeed(getContext(), 0, false, false)); + // speed is NaN + assertEquals("-", StringUtils.formatSpeed(getContext(), Double.NaN, true, true)); + // speed is infinite + assertEquals("-", StringUtils.formatSpeed(getContext(), Double.NEGATIVE_INFINITY, true, true)); + } + + /** + * Tests {@link StringUtils#formatTimeDistance(android.content.Context, long, double, boolean)}. + */ + public void testFormatTimeDistance() { + assertEquals("00:10 5.00 km", StringUtils.formatTimeDistance(getContext(), 10000, 5000, true)); + } + + /** + * Tests {@link StringUtils#formatCData(String)}. + */ + public void testFormatCData() { + assertEquals("", StringUtils.formatCData("hello")); + assertEquals("there]]>", StringUtils.formatCData("hello]]>there")); + } + + /** + * Tests {@link StringUtils#getTime(String)}. + */ + public void testGetTime() { + assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0); + assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0); + } + + /** + * Tests {@link StringUtils#getTime(String)} with fractional seconds. + */ + public void testGetTime_fractional() { + assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300); + assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350); + assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); + assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352); + } + + /** + * Tests {@link StringUtils#getTime(String)} with time zone. + */ + public void testGetTime_timezone() { + assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0); + assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0); + assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0); + assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0); + assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0); + assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0); + assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0); + } + + /** + * Tests {@link StringUtils#getTime(String)} with fractional seconds and time + * zone. + */ + public void testGetTime_fractionalAndTimezone() { + assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); + assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470); + assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579); + assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800); + } + + /** + * Asserts the {@link StringUtils#getTime(String)} returns the expected + * values. + * + * @param xmlDateTime the xml date time string + * @param year the expected year + * @param month the expected month + * @param day the expected day + * @param hour the expected hour + * @param minute the expected minute + * @param second the expected second + * @param millisecond the expected milliseconds + */ + private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, + int second, int millisecond) { + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.set(year, month - 1, day, hour, minute, second); + calendar.set(GregorianCalendar.MILLISECOND, millisecond); + assertEquals(calendar.getTimeInMillis(), StringUtils.getTime(xmlDateTime)); + } + + /** + * Tests {@link StringUtils#getTimeParts(long)} with a positive number. + */ + public void testGetTimeParts_postive() { + int parts[] = StringUtils.getTimeParts(61000); + assertEquals(1, parts[0]); + assertEquals(1, parts[1]); + assertEquals(0, parts[2]); + } + + /** + * Tests {@link StringUtils#getTimeParts(long)} with a negative number. + */ + public void testGetTimeParts_negative() { + int parts[] = StringUtils.getTimeParts(-61000); + assertEquals(-1, parts[0]); + assertEquals(-1, parts[1]); + assertEquals(0, parts[2]); } }