diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index dd893eb0d..7aae5c7f3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -15,16 +15,24 @@ */ package com.google.android.apps.mytracks; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Random; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; +import com.google.android.accounts.Account; +import com.google.android.apps.mymaps.MyMapsConstants; +import com.google.android.apps.mymaps.MyMapsList; +import com.google.android.apps.mymaps.VersionChecker; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.AuthManager; +import com.google.android.apps.mytracks.io.AuthManagerFactory; +import com.google.android.apps.mytracks.io.GpxImporter; +import com.google.android.apps.mytracks.io.SendToDocs; +import com.google.android.apps.mytracks.io.SendToMyMaps; +import com.google.android.apps.mytracks.io.TrackWriter; +import com.google.android.apps.mytracks.io.TrackWriterFactory; +import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.maps.mytracks.R; import android.app.Activity; import android.app.AlertDialog; @@ -62,24 +70,16 @@ import android.widget.RelativeLayout; import android.widget.TabHost; import android.widget.Toast; -import com.google.android.accounts.Account; -import com.google.android.apps.mymaps.MyMapsConstants; -import com.google.android.apps.mymaps.MyMapsList; -import com.google.android.apps.mymaps.VersionChecker; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManagerFactory; -import com.google.android.apps.mytracks.io.GpxSaxImporter; -import com.google.android.apps.mytracks.io.SendToDocs; -import com.google.android.apps.mytracks.io.SendToMyMaps; -import com.google.android.apps.mytracks.io.TrackWriter; -import com.google.android.apps.mytracks.io.TrackWriterFactory; -import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; -import com.google.android.apps.mytracks.services.ITrackRecordingService; -import com.google.android.apps.mytracks.services.TrackRecordingService; -import com.google.android.maps.mytracks.R; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Random; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; /** * The super activity that embeds our sub activities. @@ -958,7 +958,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, try { try { InputStream is = new FileInputStream(fileName); - trackIdsImported = GpxSaxImporter.importGPXFile(is, providerUtils); + trackIdsImported = GpxImporter.importGPXFile(is, providerUtils); } catch (SAXException e) { Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java deleted file mode 100644 index 968f3138a..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2008 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; - -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.MyTracksUtils; - -import android.location.Location; - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -/** - * Helper class to parse a GPX file or string and convert it into a track - * object. - * - * TODO: See if we can use a SAX style parser as the DOM style - * parsing uses too much memory and will not allow import of very large GPX - * files (limit currently set to 500KB). - * - * @author Leif Hendrik Wilden - */ -public class GpxImport { - private static final SimpleDateFormat DATE_FORMAT1 = - new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ"); - private static final SimpleDateFormat DATE_FORMAT2 = - new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'"); - private static final SimpleDateFormat DATE_FORMAT3 = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - - /** - * Reads GPS tracks from a GPX file and append tracks and their coordinates to - * the given list of tracks. - * - * @param tracks a list of tracks - * @param filename a file name - * @throws SAXException a parsing error - * @throws ParserConfigurationException internal error - * @throws IOException a file reading problem - */ - public static void importGPXFile( - final String filename, final ArrayList tracks) - throws SAXException, ParserConfigurationException, IOException, - OutOfMemoryError { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - File file = new File(filename); - if (file.length() > 500 * 1024) { - // Better to throw an error now then, to let the VM do it. If the VM does - // it, it will throw another exception while throwing the OutOfMemoryError - // and that will kill out app with a nasty error message. In that way we - // can at least display a reasonable message to the user and let her know - // what the problem is. - throw new OutOfMemoryError(); - } - Document doc = builder.parse(file); - importGPXDocument(tracks, doc); - } - - /** - * This is a utility class with only static members. - */ - private GpxImport() { - } - - /** - * Reads GPS tracks from a GPX document and append then to the given list of - * tracks. - * - * TODO: Break this method down into smaller ones - * - * @param tracks an array list of tracks - * @param doc a document - */ - public static void importGPXDocument( - final ArrayList tracks, final Document doc) { - NodeList trackNodes = doc.getElementsByTagName("trk"); - for (int n = 0; n < trackNodes.getLength(); n++) { - Track track = new Track(); - tracks.add(track); - ArrayList locations = track.getLocations(); - Location lastLocation = null; - - Node trkNode = trackNodes.item(n); - NodeList segmentNodes = trkNode.getChildNodes(); - ArrayList nodes = new ArrayList(); - int nSegments = 0; - for (int k = 0; k < segmentNodes.getLength(); k++) { - Node segmentNode = segmentNodes.item(k); - if (segmentNode.getNodeName().equals("name")) { - track.setName(segmentNode.getFirstChild().getNodeValue()); - } else if (segmentNode.getNodeName().equals("description")) { - track.setDescription(segmentNode.getFirstChild().getNodeValue()); - } else if (segmentNode.getNodeName().equals("trkseg")) { - if (nSegments > 0) { - // Add a segment separator: - Location location = new Location("gps"); - location.setLatitude(100.0); - location.setLongitude(100.0); - location.setAltitude(0); - if (locations.size() > 0) { - long pointTime = locations.get(locations.size() - 1).getTime(); - location.setTime(pointTime); - } - track.addLocation(location); - lastLocation = null; - } - nSegments++; - NodeList segmentChildren = segmentNode.getChildNodes(); - nodes.clear(); - for (int j = 0; j < segmentChildren.getLength(); j++) { - Node child = segmentChildren.item(j); - if (child.getNodeName().equals("trkpt")) { - nodes.add(child); - } - } - for (int i = 0; i < nodes.size(); i++) { - NamedNodeMap namedNodes = nodes.get(i).getAttributes(); - double lat = Double.parseDouble(namedNodes.getNamedItem("lat") - .getNodeValue()); - double lon = Double.parseDouble(namedNodes.getNamedItem("lon") - .getNodeValue()); - NodeList children = nodes.get(i).getChildNodes(); - Node elementNode = null; - Node timeNode = null; - for (int j = 0; j < children.getLength(); j++) { - Node child = children.item(j); - if (child.getNodeName().equals("ele")) { - elementNode = child; - } else if (child.getNodeName().equals("time")) { - timeNode = child; - } - } - String altitudeStr = null; - if (elementNode != null) { - altitudeStr = elementNode.getFirstChild().getNodeValue(); - } else { - altitudeStr = "0"; - } - String timeContents = null; - if (timeNode != null) { - timeContents = timeNode.getFirstChild().getNodeValue(); - } else { - timeContents = ""; - } - double altitude = Double.parseDouble(altitudeStr); - long t = -1; - try { - // 1st try with time zone at end a la "+0000" - t = DATE_FORMAT1.parse(timeContents).getTime(); - } catch (ParseException e) { - // if that fails, try with a literal "Z" at the end (this is not - // according to xml standard, but some gpx files are like that): - try { - t = DATE_FORMAT2.parse(timeContents).getTime(); - } catch (ParseException ex) { - // some gpx timestamps have 3 additional digits at the end. - try { - t = DATE_FORMAT3.parse(timeContents).getTime(); - } catch (ParseException exc) { - t = 0; - } - } - } - - Location location = new Location("gps"); - location.setLatitude(lat); - location.setLongitude(lon); - location.setAltitude(altitude); - location.setTime(t); - // We don't have a speed and bearing in GPX, make something up from - // the last two points: - if (lastLocation != null) { - final long dt = location.getTime() - lastLocation.getTime(); - if (dt > 0) { - final float speed = - location.distanceTo(lastLocation) / (dt / 1000); - location.setSpeed(speed); - } - location.setBearing(lastLocation.bearingTo(location)); - } - lastLocation = location; - if (MyTracksUtils.isValidLocation(location)) { - track.addLocation(location); - } - } - - if (locations.size() > 0) { - long startTime = locations.get(0).getTime(); - track.setStartTime(startTime); - - // Calculate statistics for the imported track - TripStatistics stats = new TripStatistics(startTime); - for (Location location : locations) { - if (MyTracksUtils.isValidLocation(location)) { - /* Any time works here. The totalTime will be set by "pauseAt" later: */ - stats.addLocation(location, location.getTime()); - } - } - long lastPointTime = locations.get(locations.size() - 1).getTime(); - stats.pauseAt(lastPointTime); - track.setStopTime(lastPointTime); - - stats.fillStatisticsForTrack(track); - } - } - } - } - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxSaxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java similarity index 67% rename from MyTracks/src/com/google/android/apps/mytracks/io/GpxSaxImporter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java index a4705318d..344b9109d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxSaxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -15,48 +15,51 @@ */ package com.google.android.apps.mytracks.io; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.MyTracksUtils; + +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; + import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; +import java.util.SimpleTimeZone; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; +import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; -import android.location.Location; -import android.location.LocationManager; -import android.net.Uri; - -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.MyTracksUtils; - /** * Imports GPX XML files to the my tracks provider * * @author Leif Hendrik Wilden - * @author Steffen (steffen.horlacher@gmail.com) + * @author Steffen Horlacher */ -public class GpxSaxImporter extends DefaultHandler { +public class GpxImporter extends DefaultHandler { /** - * Different data formats used in GPX files + * Different date formats used in GPX files */ - private static final SimpleDateFormat DATE_FORMAT1 = new SimpleDateFormat( - "yyyy-MM-dd'T'hh:mm:ssZ"); - private static final SimpleDateFormat DATE_FORMAT2 = new SimpleDateFormat( - "yyyy-MM-dd'T'hh:mm:ss'Z'"); - private static final SimpleDateFormat DATE_FORMAT3 = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - + static final SimpleDateFormat DATE_FORMAT1 = new SimpleDateFormat( + "yyyy-MM-dd'T'hh:mm:ssZ"); + static final SimpleDateFormat DATE_FORMAT2 = new SimpleDateFormat( + "yyyy-MM-dd'T'hh:mm:ss'Z'"); + static final SimpleDateFormat DATE_FORMAT3 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + static final SimpleTimeZone UTC_TIMEZONE = new SimpleTimeZone(0, "UTC"); + /** * GPX-XML tag names and attributes */ @@ -69,6 +72,14 @@ public class GpxSaxImporter extends DefaultHandler { private static final String ATT_LAT = "lat"; private static final String ATT_LON = "lon"; + final private MyTracksProviderUtils providerUtils; + + /** + * List of track ids written in the database. Only contains successfully + * written tracks. + */ + final private List tracksWritten; + /** * Contains the current elements content */ @@ -100,33 +111,31 @@ public class GpxSaxImporter extends DefaultHandler { private int numberOfLocations; /** - * List of track ids written in the database does only contain successful - * finished ones - */ - private List tracksWritten; - - /** - * used to identify if a track was written to the database but not yet finish - * successfully + * Used to identify if a track was written to the database but not yet + * finished successfully. */ private boolean isCurrentTrackRollbackable; - private MyTracksProviderUtils providerUtils; - /** - * flag to indicate if we in a track xml element some sub elements like name + * Flag to indicate if we in a track xml element some sub elements like name * may be used in other parts of the gpx file - ignore them */ private boolean isInTrackElement; /** - * Reads GPS tracks from a GPX file and append tracks and their coordinates to - * the given list of tracks. + * SAX-Locator to get current line information + */ + private Locator locator; + + /** + * Reads GPS tracks from a GPX file and writes tracks and their coordinates to + * the database. * * @param tracks * a list of tracks * @param is * a input steam with gpx-xml data + * @return long[] array of track ids written in the database * @throws SAXException * a parsing error * @throws ParserConfigurationException @@ -135,11 +144,11 @@ public class GpxSaxImporter extends DefaultHandler { * a file reading problem */ public static long[] importGPXFile(final InputStream is, - final MyTracksProviderUtils providerUtils) - throws ParserConfigurationException, SAXException, IOException { + final MyTracksProviderUtils providerUtils) + throws ParserConfigurationException, SAXException, IOException { SAXParserFactory factory = SAXParserFactory.newInstance(); - GpxSaxImporter handler = new GpxSaxImporter(providerUtils); + GpxImporter handler = new GpxImporter(providerUtils); SAXParser parser = factory.newSAXParser(); long[] trackIds = null; @@ -157,7 +166,7 @@ public class GpxSaxImporter extends DefaultHandler { /** * Constructor, requires providerUtils for writing tracks the database. */ - public GpxSaxImporter(MyTracksProviderUtils providerUtils) { + public GpxImporter(MyTracksProviderUtils providerUtils) { this.providerUtils = providerUtils; tracksWritten = new ArrayList(); content = new StringBuilder(); @@ -170,9 +179,19 @@ public class GpxSaxImporter extends DefaultHandler { @Override public void startElement(String uri, String localName, String name, - Attributes attributes) throws SAXException { + Attributes attributes) throws SAXException { + + // reset element content + content.setLength(0); if (localName.equalsIgnoreCase(TAG_TRACK)) { + + // test if we are already in a track element - abort in this case + if (isInTrackElement) { + String msg = createErrorMessage("Invalid GPX-XML detected"); + throw new SAXException(msg); + } + isInTrackElement = true; onTrackElementStart(); @@ -187,7 +206,7 @@ public class GpxSaxImporter extends DefaultHandler { @Override public void endElement(String uri, String localName, String name) - throws SAXException { + throws SAXException { if (localName.equalsIgnoreCase(TAG_TRACK)) { onTrackElementEnd(); @@ -213,6 +232,11 @@ public class GpxSaxImporter extends DefaultHandler { content.setLength(0); } + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + /** * Create a new Track object and insert empty track in database. Track will be * updated with missing values later. @@ -267,8 +291,11 @@ public class GpxSaxImporter extends DefaultHandler { /** * Track point finished, write in database + * + * @throws SAXException + * - thrown if track point is invalid */ - private void onTrackPointElementEnd() { + private void onTrackPointElementEnd() throws SAXException { if (MyTracksUtils.isValidLocation(location)) { @@ -276,7 +303,7 @@ public class GpxSaxImporter extends DefaultHandler { // insert in db Uri trackPointIdUri = providerUtils.insertTrackPoint(location, track - .getId()); + .getId()); // set start and stop id for track long trackPointId = Long.parseLong(trackPointIdUri.getLastPathSegment()); @@ -291,6 +318,11 @@ public class GpxSaxImporter extends DefaultHandler { lastLocation = location; numberOfLocations++; + } else { + + // invalid location - abort import + String msg = createErrorMessage("Invalid location detected: " + location); + throw new SAXException(msg); } } @@ -307,8 +339,9 @@ public class GpxSaxImporter extends DefaultHandler { track.setNumberOfPoints(numberOfLocations); stats.fillStatisticsForTrack(track); providerUtils.updateTrack(track); - tracksWritten.add(new Long(track.getId())); + tracksWritten.add(track.getId()); isCurrentTrackRollbackable = false; + lastLocation = null; } else { @@ -320,11 +353,14 @@ public class GpxSaxImporter extends DefaultHandler { } /** - * setting time and doing additional calculations as this is the last value + * Setting time and doing additional calculations as this is the last value * required. Also sets the start time for track and statistics as there is no * start time in the track root element + * + * @throws SAXException + * on parsing errors */ - private void onTimeElementEnd() { + private void onTimeElementEnd() throws SAXException { long time = parseTimeForAllFormats(content.toString().trim()); @@ -337,9 +373,11 @@ public class GpxSaxImporter extends DefaultHandler { track.setStartTime(time); } - // We don't have a speed and bearing in GPX, make - // something up from - // the last two points: + // We don't have a speed and bearing in GPX, make something up from + // the last two points. + // TODO GPS points tend to have some inherent imprecision, + // speed and bearing will likely be off, so the statistics for things like + // max speed will also be off. if (lastLocation != null) { final long dt = location.getTime() - lastLocation.getTime(); if (dt > 0) { @@ -354,10 +392,7 @@ public class GpxSaxImporter extends DefaultHandler { private void onAltitudeElementEnd() { if (location != null) { String altitude = content.toString().trim(); - // make altitude optional - if (altitude != null) { - location.setAltitude(Double.parseDouble(altitude)); - } + location.setAltitude(Double.parseDouble(altitude)); } } @@ -391,33 +426,67 @@ public class GpxSaxImporter extends DefaultHandler { * Parse time trying different formats used in GPX files * * @param timeContents - * string with time infomation + * string with time information * @return time as long + * @throws SAXException + * on time parsing errors */ - private long parseTimeForAllFormats(String timeContents) { + private long parseTimeForAllFormats(String timeContents) throws SAXException { - long t = -1; - - try { - // 1st try with time zone at end a la "+0000" - t = DATE_FORMAT1.parse(timeContents).getTime(); - } catch (ParseException e) { - // if that fails, try with a literal "Z" at the end - // (this is not - // according to xml standard, but some gpx files are - // like that): - try { - t = DATE_FORMAT2.parse(timeContents).getTime(); - } catch (ParseException ex) { - // some gpx timestamps have 3 additional digits - // at the end. - try { - t = DATE_FORMAT3.parse(timeContents).getTime(); - } catch (ParseException exc) { - t = 0; - } - } + long time = -1; + + // 1st try with time zone at end a la "+0000" + time = parseTime(timeContents, DATE_FORMAT1); + if(time > -1) { + return time; } - return t; + + // if that fails, try with a literal "Z" at the end + // (this is not according to xml standard, but some gpx files are like + // that): + time = parseTime(timeContents, DATE_FORMAT2); + if(time > -1) { + return time; + } + + // some gpx timestamps have 3 additional digits at the end. + time = parseTime(timeContents, DATE_FORMAT3); + if(time > -1) { + return time; + } + + // everything failed - abort the import + String msg = createErrorMessage("Invalid time format: " + timeContents); + throw new SAXException(msg); } + + private long parseTime(String timeContents, SimpleDateFormat formatParam) { + SimpleDateFormat format = formatParam; + try { + format.setTimeZone(UTC_TIMEZONE); + return format.parse(timeContents).getTime(); + } catch (ParseException ex) { + // do nothing + } + return -1; + } + + /** + * Builds an parsing error message with current line information + * + * @param details + * details about the error, will be appended + * @return error message string with current line information + */ + private String createErrorMessage(String details) { + StringBuffer msg = new StringBuffer(); + msg.append("Parsing error at line: "); + msg.append(locator.getLineNumber()); + msg.append(" column: "); + msg.append(locator.getColumnNumber()); + msg.append(". "); + msg.append(details); + return msg.toString(); + } + } diff --git a/MyTracksTest/lib/AndroidMockGenerator.jar b/MyTracksTest/lib/AndroidMockGenerator.jar new file mode 100644 index 000000000..ef8b3612c Binary files /dev/null and b/MyTracksTest/lib/AndroidMockGenerator.jar differ diff --git a/MyTracksTest/lib/AndroidMockRuntime.jar b/MyTracksTest/lib/AndroidMockRuntime.jar new file mode 100644 index 000000000..40fa9e173 Binary files /dev/null and b/MyTracksTest/lib/AndroidMockRuntime.jar differ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java index fdecc33c9..4b7419c2c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java @@ -1,10 +1,18 @@ package com.google.android.apps.mytracks.io; -import static org.easymock.EasyMock.capture; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.expectLastCall; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackPointsColumns; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; +import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; +import com.google.android.testing.mocking.AndroidMock; +import com.google.android.testing.mocking.UsesMocks; + +import android.content.ContentUris; +import android.location.Location; +import android.net.Uri; +import android.test.AndroidTestCase; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -14,33 +22,16 @@ import java.text.SimpleDateFormat; import javax.xml.parsers.ParserConfigurationException; import org.easymock.Capture; -import org.easymock.EasyMock; import org.xml.sax.SAXException; -import android.content.ContentUris; -import android.location.Location; -import android.net.Uri; -import android.test.AndroidTestCase; - -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackPointsColumns; -import com.google.android.apps.mytracks.content.TracksColumns; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; -import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; - /** * Tests for the GPX importer. * - * @author Steffen (steffen.horlacher@gmail.com) + * @author Steffen Horlacher */ public class GpxImporterTest extends AndroidTestCase { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat( - "yyyy-MM-dd'T'hh:mm:ss'Z'"); - private static final String TRACK_NAME = "blablub"; - private static final String TRACK_DESC = "s'Laebe isch koi Schlotzer"; private static final String TRACK_LAT_1 = "48.768364"; @@ -55,35 +46,44 @@ public class GpxImporterTest extends AndroidTestCase { // TODO use real files from different sources with more track points private static final String VALID_TEST_GPX = "" + "" + TRACK_ELE_1 + " +" + "" + TRACK_ELE_2 + "" + ""; + + TRACK_NAME + "]]>" + "" + TRACK_ELE_1 + " +" + "" + TRACK_ELE_2 + + "" + + ""; // invalid xml - private static final String INVALID_TEST_GPX = VALID_TEST_GPX.substring(0, - VALID_TEST_GPX.length() - 50); + private static final String INVALID_XML_TEST_GPX = VALID_TEST_GPX + .substring(0, VALID_TEST_GPX.length() - 50); + private static final String INVALID_LOCATION_TEST_GPX = VALID_TEST_GPX + .replaceAll(TRACK_LAT_1, "1000.0"); + private static final String INVALID_TIME_TEST_GPX = VALID_TEST_GPX + .replaceAll(TRACK_TIME_1, "invalid"); private static final long TRACK_ID = 1; - private static final long TRACK_POINT_ID = 1; - + private static final long TRACK_POINT_ID_1 = 1; + private static final long TRACK_POINT_ID_2 = 1; + private static final Uri TRACK_ID_URI = ContentUris.appendId( - TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID).build(); - private static final Uri TRACK_POINT_ID_URI = ContentUris.appendId( - TrackPointsColumns.CONTENT_URI.buildUpon(), TRACK_POINT_ID).build(); + TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID).build(); + private static final Uri TRACK_POINT_ID_URI_1 = ContentUris.appendId( + TrackPointsColumns.CONTENT_URI.buildUpon(), TRACK_POINT_ID_1).build(); + private static final Uri TRACK_POINT_ID_URI_2 = ContentUris.appendId( + TrackPointsColumns.CONTENT_URI.buildUpon(), TRACK_POINT_ID_2).build(); private MyTracksProviderUtils providerUtils; private Factory oldProviderUtilsFactory; + @UsesMocks(MyTracksProviderUtils.class) @Override protected void setUp() throws Exception { super.setUp(); - providerUtils = EasyMock.createMock(MyTracksProviderUtils.class); + providerUtils = AndroidMock.createMock(MyTracksProviderUtils.class); oldProviderUtilsFactory = TestingProviderUtilsFactory - .installWithInstance(providerUtils); + .installWithInstance(providerUtils); } @Override @@ -98,68 +98,123 @@ public class GpxImporterTest extends AndroidTestCase { public void testImportSuccess() throws Exception { Capture trackParam = new Capture(); - Capture locParam = new Capture(); - Capture idParam = new Capture(); + Capture locParam1 = new MyLocationCaptue(); + Capture locParam2 = new MyLocationCaptue(); - expect(providerUtils.insertTrack(capture(trackParam))) - .andReturn(TRACK_ID_URI); + AndroidMock.expect( + providerUtils.insertTrack(AndroidMock.capture(trackParam))) + .andReturn(TRACK_ID_URI); - expect(providerUtils.insertTrackPoint(capture(locParam), capture(idParam))) - .andReturn(TRACK_POINT_ID_URI); - expectLastCall().times(2); + AndroidMock.expect( + providerUtils.insertTrackPoint(AndroidMock.capture(locParam1), + AndroidMock.anyLong())).andReturn(TRACK_POINT_ID_URI_1); - providerUtils.updateTrack(capture(trackParam)); + AndroidMock.expect( + providerUtils.insertTrackPoint(AndroidMock.capture(locParam2), + AndroidMock.anyLong())).andReturn(TRACK_POINT_ID_URI_2); - replay(providerUtils); + providerUtils.updateTrack(AndroidMock.capture(trackParam)); + + AndroidMock.replay(providerUtils); InputStream is = new ByteArrayInputStream(VALID_TEST_GPX.getBytes()); - GpxSaxImporter.importGPXFile(is, providerUtils); + GpxImporter.importGPXFile(is, providerUtils); - verify(); + AndroidMock.verify(); + + SimpleDateFormat format = GpxImporter.DATE_FORMAT2; // verify track parameter Track track = trackParam.getValue(); assertEquals(TRACK_NAME, track.getName()); assertEquals(TRACK_DESC, track.getDescription()); - assertEquals(DATE_FORMAT.parse(TRACK_TIME_1).getTime(), track.getStartTime()); + assertEquals(format.parse(TRACK_TIME_1).getTime(), track.getStartTime()); assertNotSame(-1, track.getStartId()); assertNotSame(-1, track.getStopId()); // verify last location parameter - Location loc = locParam.getValue(); - assertEquals(Double.parseDouble(TRACK_LAT_2), loc.getLatitude()); - assertEquals(Double.parseDouble(TRACK_LON_2), loc.getLongitude()); - assertEquals(Double.parseDouble(TRACK_ELE_2), loc.getAltitude()); - assertEquals(DATE_FORMAT.parse(TRACK_TIME_2).getTime(), loc.getTime()); + Location loc1 = locParam1.getValue(); + assertEquals(Double.parseDouble(TRACK_LAT_1), loc1.getLatitude()); + assertEquals(Double.parseDouble(TRACK_LON_1), loc1.getLongitude()); + assertEquals(Double.parseDouble(TRACK_ELE_1), loc1.getAltitude()); + assertEquals(format.parse(TRACK_TIME_1).getTime(), loc1.getTime()); + Location loc2 = locParam2.getValue(); + assertEquals(Double.parseDouble(TRACK_LAT_2), loc2.getLatitude()); + assertEquals(Double.parseDouble(TRACK_LON_2), loc2.getLongitude()); + assertEquals(Double.parseDouble(TRACK_ELE_2), loc2.getAltitude()); + assertEquals(format.parse(TRACK_TIME_2).getTime(), loc2.getTime()); } /** - * Test if created track will be deleted on parsing errors + * Test with invalid location - track should be deleted */ - public void testImportFailure() throws ParserConfigurationException, - SAXException, IOException { + public void testImportLocationFailure() throws ParserConfigurationException, + SAXException, IOException { - expect( - providerUtils.insertTrack((Track) EasyMock.anyObject())).andReturn(TRACK_ID_URI); - expect( - providerUtils.insertTrackPoint((Location) EasyMock.anyObject(), - EasyMock.anyLong())).andReturn(TRACK_POINT_ID_URI); + testInvalidXML(INVALID_LOCATION_TEST_GPX); + } + + + /** + * Test with invalid time - track should be deleted + */ + public void testImportTimeFailure() throws ParserConfigurationException, + SAXException, IOException { + + testInvalidXML(INVALID_TIME_TEST_GPX); + } + + /** + * Test with invalid xml - track should be deleted + */ + public void testImportXMLFailure() throws ParserConfigurationException, + SAXException, IOException { + + testInvalidXML(INVALID_XML_TEST_GPX); + } + + private void testInvalidXML(String xml) throws ParserConfigurationException, + IOException { + + AndroidMock.expect( + providerUtils.insertTrack((Track) AndroidMock.anyObject())) + .andReturn(TRACK_ID_URI); + + AndroidMock.expect( + providerUtils.insertTrackPoint((Location) AndroidMock.anyObject(), + AndroidMock.anyLong())).andStubReturn(TRACK_POINT_ID_URI_1); - expectLastCall().anyTimes(); providerUtils.deleteTrack(TRACK_ID); - replay(providerUtils); + AndroidMock.replay(providerUtils); - InputStream is = new ByteArrayInputStream(INVALID_TEST_GPX.getBytes()); + InputStream is = new ByteArrayInputStream(xml.getBytes()); try { - GpxSaxImporter.importGPXFile(is, providerUtils); + GpxImporter.importGPXFile(is, providerUtils); } catch (SAXException e) { // expected exception } - verify(); + AndroidMock.verify(); + + } + + /** + * Workaround because of capture bug 2617107 in easymock: + * + * http://sourceforge.net/tracker/?func=detail&aid=2617107&group_id=82958&atid=567837 + */ + @SuppressWarnings("serial") + class MyLocationCaptue extends Capture { + + @Override + public void setValue(Location value) { + if (!hasCaptured()) { + super.setValue(value); + } + } }