diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index b20a649d3..91f13973e 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -2596,13 +2596,53 @@ limitations under the License. Screenshot: value_auto_resume.png"> 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 5433767db..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 @@ -22,7 +22,7 @@ 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; @@ -130,7 +130,7 @@ public class CsvTrackWriter implements TrackFormatWriter { Double.toString(location.getBearing()), SHORT_FORMAT.format(location.getAccuracy()), SHORT_FORMAT.format(location.getSpeed()), - FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime())); + StringUtils.formatDateTimeIso8601(location.getTime())); } @Override @@ -211,7 +211,7 @@ public class CsvTrackWriter implements TrackFormatWriter { Double.toString(location.getBearing()), SHORT_FORMAT.format(location.getAccuracy()), SHORT_FORMAT.format(location.getSpeed()), - FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()), + StringUtils.formatDateTimeIso8601(location.getTime()), power, cadence, heartRate, 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..0845e0c57 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; @@ -107,9 +106,9 @@ 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(); @@ -142,7 +141,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)) + ""); } } @@ -171,8 +170,8 @@ public class KmlTrackWriter implements TrackFormatWriter { writePlacemark(name, track.getDescription(), START_STYLE, firstLocation); 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 +231,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 +307,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( @@ -356,7 +354,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/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index 2613aab80..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,7 +22,6 @@ 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; @@ -31,7 +30,6 @@ import android.location.Location; import java.io.OutputStream; import java.io.PrintWriter; -import java.util.Date; import java.util.Locale; /** @@ -133,7 +131,7 @@ public class TcxTrackWriter implements TrackFormatWriter { if (printWriter != null) { printWriter.println(""); printWriter.println("" - + StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + ""); // , , and are required by type=Application_t. printWriter.println(""); @@ -149,8 +147,7 @@ public class TcxTrackWriter implements TrackFormatWriter { @Override public void writeBeginTrack(Location firstPoint) { if (printWriter != null) { - String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format( - track.getStatistics().getStartTime()); + String startTime = StringUtils.formatDateTimeIso8601(track.getStatistics().getStartTime()); long totalTimeInSeconds = track.getStatistics().getTotalTime() / 1000; printWriter.println(""); @@ -173,7 +170,7 @@ public class TcxTrackWriter implements TrackFormatWriter { printWriter.println(""); printWriter.println(""); printWriter.println("" - + StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + ""); // , , and are required for type=Device_t. printWriter.println("0"); @@ -203,8 +200,7 @@ public class TcxTrackWriter implements TrackFormatWriter { public void writeLocation(Location location) { if (printWriter != null) { printWriter.println(""); - printWriter.println(""); + printWriter.println(""); printWriter.println(""); printWriter.println("" + location.getLatitude() + ""); printWriter.println("" + location.getLongitude() + ""); 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/StringUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java index 70662a776..c7980a1fb 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java @@ -21,12 +21,10 @@ 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; @@ -38,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. * @@ -58,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); } /** @@ -70,10 +89,10 @@ public class StringUtils { public static String formatElapsedTime(long time) { return DateUtils.formatElapsedTime(time / 1000); } - + /** * Formats the elapsed time in the form "H:MM:SS". - * + * * @param time the time in milliseconds */ public static String formatElapsedTimeWithHour(long time) { @@ -82,103 +101,107 @@ public class StringUtils { } /** - * Formats the elapsed time and total distance. + * Formats the distance. * - * @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 context the context + * @param distance the distance in meters + * @param metric true to use metric. False to use imperial */ - public static String formatTimeDistance(Context context, double totalDistance, long totalTime, boolean metric) { - String distanceUnit; + 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(); @@ -186,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; @@ -204,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) { @@ -235,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/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java index a120b4999..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 @@ -17,7 +17,7 @@ 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; @@ -70,8 +70,7 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { * @param location the location */ private void assertTagMatchesLocation(Element tag, MyTracksLocation location) { - assertEquals( - FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()), getChildTextValue(tag, "Time")); + assertEquals(StringUtils.formatDateTimeIso8601(location.getTime()), getChildTextValue(tag, "Time")); Element positionTag = getChildElement(tag, "Position"); assertEquals( 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 0df86e089..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,69 +15,43 @@ */ 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)); } - - 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); - } - - 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); - } - - 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#formatDateTimeIso8601(long)}. + */ + public void testFormatDateTimeIso8601() { + assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345)); } /** @@ -119,4 +93,139 @@ public class StringUtilsTest extends TestCase { // 100 hours assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000)); } + + /** + * 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]); + } }