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 11ed37a40..c8a17aa66 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; @@ -112,9 +111,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(); @@ -147,7 +146,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)) + ""); } } @@ -176,8 +175,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"); @@ -237,8 +236,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() + ""); @@ -314,8 +312,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( @@ -361,7 +359,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/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/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/io/maps/ChooseMapActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivityTest.java new file mode 100644 index 000000000..051a28289 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivityTest.java @@ -0,0 +1,146 @@ +/* + * 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. + * + * @throws InterruptedException + */ + 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/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]); + } }