diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 015c9746e..0a83934c5 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -45,7 +45,8 @@ limitations under the License. - + + diff --git a/MyTracks/res/menu/track_list.xml b/MyTracks/res/menu/track_list.xml index b93d1a72f..3d5aae419 100644 --- a/MyTracks/res/menu/track_list.xml +++ b/MyTracks/res/menu/track_list.xml @@ -28,17 +28,34 @@ limitations under the License. android:title="@string/menu_start_gps"/> + android:id="@+id/track_list_import_all" + android:title="@string/menu_import_all"> + + + + + - - - - + + + + - - @string/import_gpx_files_zero - @string/import_gpx_files_one - @string/import_gpx_files_two - @string/import_gpx_files_few - @string/import_gpx_files_many - @string/import_gpx_files_other + + @string/import_files_zero + @string/import_files_one + @string/import_files_two + @string/import_files_few + @string/import_files_many + @string/import_files_other @string/voice_hours_zero diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index a4dba9ad1..16f7a9910 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -604,39 +604,13 @@ limitations under the License. that the operation was canceled."> The operation was canceled. - - No external storage is found. + + External storage not available. - - Unable to import from external storage. - - - No file found in %1$s. - - - Importing from external storage… - - - Finished importing from external storage. - - - Imported %1$d of %2$s from %3$s. + + External storage not writeable. - %1$d GPX files + %1$d files - - %1$d GPX files + %1$d files - - 1 GPX file + + 1 file - - %1$d GPX files + %1$d files - - 2 GPX files + + 2 files - - 0 GPX files + + 0 files + + + %1$s does not exist. + + + No file found in %1$s. + + + Importing from %1$s… + + + Imported %1$d of %2$s from %3$s. @@ -1149,11 +1162,19 @@ limitations under the License. Character limit: ~20 (1 line)"> Help - - Import from external storage + Import all from external storage + + + Import %1$s + Unable to restore from backup. + + + Finished restoring from backup. + + { @Override protected Boolean doInBackground(Void... params) { - if (!FileUtils.isSdCardAvailable()) { - messageId = R.string.external_storage_error_no_storage; + if (!FileUtils.isExternalStorageWriteable()) { + messageId = R.string.external_storage_not_writeable; return false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java index 82fd11e04..6993c85a7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java @@ -93,7 +93,7 @@ class ExternalFileBackup { return FileUtils.ensureDirectoryExists(dir) ? dir : null; } else { // Return it if it already exists, otherwise return null - return dir.isDirectory() ? dir : null; + return FileUtils.isDirectory(dir) ? dir : null; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java index cf4e008dd..665f0e48a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java @@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask { this.externalFileBackup = new ExternalFileBackup(restoreActivity); success = false; completed = false; - messageId = R.string.external_storage_import_error; + messageId = R.string.restore_error; } /** @@ -85,7 +85,7 @@ public class RestoreAsyncTask extends AsyncTask { protected Boolean doInBackground(Void... params) { try { externalFileBackup.restoreFromDate(date); - messageId = R.string.external_storage_import_success; + messageId = R.string.restore_success; return true; } catch (IOException e) { Log.d(TAG, "IO exception", e); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java index 0a5955c15..632c8e85b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java @@ -57,8 +57,8 @@ public class RestoreChooserActivity extends Activity { ExternalFileBackup externalFileBackup = new ExternalFileBackup(this); // Get the list of existing backups - if (!FileUtils.isSdCardAvailable()) { - Toast.makeText(this, R.string.external_storage_error_no_storage, Toast.LENGTH_LONG).show(); + if (!FileUtils.isExternalStorageAvailable()) { + Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show(); finish(); return; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java new file mode 100644 index 000000000..2de8d6d13 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java @@ -0,0 +1,571 @@ +/* + * 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.file; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl; +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.content.Waypoint.WaypointType; +import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.stats.TripStatisticsUpdater; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.PreferencesUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.TrackIconUtils; +import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; + +import android.content.Context; +import android.database.Cursor; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Abstract class for various importers like {@link GpxImporter} and + * {@link KmlImporter}. + * + * @author Jimmy Shih + */ +abstract class AbstractImporter extends DefaultHandler { + + /** + * Data for the current track. + * + * @author Jimmy Shih + */ + private class TrackData { + + // The current track + Track track = new Track(); + + // The number of segments processed for the current track + int numberOfSegments = 0; + + /* + * The last location in the current segment. Null if the current segment + * doesn't have a last location. + */ + Location lastLocationInCurrentSegment; + + // The number of locations processed for the current track + int numberOfLocations = 0; + + // The trip statistics updater for the current track + TripStatisticsUpdater tripStatisticsUpdater; + + // The import time of the track. + long importTime = System.currentTimeMillis(); + + // The buffered locations + Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS]; + + // The number of buffered locations + int numBufferedLocations = 0; + } + + protected static final String DEFAULT_ALTITUDE = "0"; + + // The maximum number of buffered locations for bulk-insertion + private static final int MAX_BUFFERED_LOCATIONS = 512; + + private final Context context; + private final MyTracksProviderUtils myTracksProviderUtils; + private final int minRecordingDistance; + private final List trackIds; + private final List waypoints; + + // The current track data + private TrackData trackData; + + // The SAX locator to get the current line information + private Locator locator; + + // The current element content + protected String content; + + protected String name; + protected String description; + protected String category; + protected String latitude; + protected String longitude; + protected String altitude; + protected String time; + protected String waypointType; + + protected AbstractImporter(Context context) { + this(context, MyTracksProviderUtils.Factory.get(context)); + } + + @VisibleForTesting + protected AbstractImporter( + Context context, MyTracksProviderUtils myTracksProviderUtils) { + this.context = context; + this.myTracksProviderUtils = myTracksProviderUtils; + this.minRecordingDistance = PreferencesUtils.getInt(context, R.string.min_recording_distance_key, + PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + trackIds = new ArrayList(); + waypoints = new ArrayList(); + } + + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + String newContent = new String(ch, start, length); + if (content == null) { + content = newContent; + } else { + /* + * In 99% of the cases, a single call to this method will be made for each + * sequence of characters we're interested in, so we'll rarely be + * concatenating strings, thus not justifying the use of a StringBuilder. + */ + content += newContent; + } + } + + /** + * Imports a file. + * + * @param inputStream the input stream. + * @return an array of imported track ids. + */ + public long[] importFile(InputStream inputStream) throws Exception { + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + SAXParser saxParser = saxParserFactory.newSAXParser(); + + try { + long start = System.currentTimeMillis(); + + saxParser.parse(inputStream, this); + + long end = System.currentTimeMillis(); + Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms"); + } catch (SAXException e) { + handleException(e); + } catch (IOException e) { + handleException(e); + } + long[] result = new long[trackIds.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = trackIds.get(i); + } + return result; + } + + /** + * On file end. + */ + protected void onFileEnd() { + // Add waypoints to the last imported track + int size = trackIds.size(); + if (size == 0) { + return; + } + long trackId = trackIds.get(size - 1); + Track track = myTracksProviderUtils.getTrack(trackId); + if (track == null) { + return; + } + Cursor trackPointCursor = null; + try { + trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false); + if (trackPointCursor == null) { + return; + } + int waypointPosition = -1; + Waypoint waypoint = null; + int trackPointPosition = -1; + Location trackPoint = null; + TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater( + track.getTripStatistics().getStartTime()); + TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater( + track.getTripStatistics().getStartTime()); + + while (true) { + if (waypoint == null) { + waypointPosition++; + waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null; + if (waypoint == null) { + // No more waypoints + return; + } + } + if (trackPoint == null) { + trackPointPosition++; + trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils + .createTrackPoint(trackPointCursor) + : null; + if (trackPoint == null) { + // No more track points. Ignore the rest of the waypoints. + return; + } + trackTripStatisticstrackUpdater.addLocation(trackPoint, minRecordingDistance); + markerTripStatisticsUpdater.addLocation(trackPoint, minRecordingDistance); + } + if (waypoint.getLocation().getTime() > trackPoint.getTime()) { + trackPoint = null; + } else if (waypoint.getLocation().getTime() < trackPoint.getTime()) { + waypoint = null; + } else { + // The waypoint location time matches the track point time + if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude() + && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) { + + // Get tripStatistics, description, and icon + TripStatistics tripStatistics; + String waypointDescription; + String icon; + if (waypoint.getType() == WaypointType.STATISTICS) { + tripStatistics = markerTripStatisticsUpdater.getTripStatistics(); + markerTripStatisticsUpdater = new TripStatisticsUpdater(trackPoint.getTime()); + waypointDescription = new DescriptionGeneratorImpl(context) + .generateWaypointDescription(tripStatistics); + icon = context.getString(R.string.marker_statistics_icon_url); + } else { + tripStatistics = null; + waypointDescription = waypoint.getDescription(); + icon = context.getString(R.string.marker_waypoint_icon_url); + } + + // Get length and duration + double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance(); + long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime(); + + // Insert waypoint + Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, + waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration, + -1L, -1L, trackPoint, tripStatistics); + myTracksProviderUtils.insertWaypoint(newWaypoint); + } + waypoint = null; + } + } + } finally { + if (trackPointCursor != null) { + trackPointCursor.close(); + } + } + } + + /** + * On track start. + */ + protected void onTrackStart() { + trackData = new TrackData(); + Uri uri = myTracksProviderUtils.insertTrack(trackData.track); + long id = Long.parseLong(uri.getLastPathSegment()); + trackIds.add(id); + trackData.track.setId(id); + } + + /** + * On track end. + */ + protected void onTrackEnd() { + flushLocations(trackData); + if (name != null) { + trackData.track.setName(name); + } + if (description != null) { + trackData.track.setDescription(description); + } + if (category != null) { + trackData.track.setCategory(category); + trackData.track.setIcon(TrackIconUtils.getIconValue(context, category)); + } + if (trackData.tripStatisticsUpdater == null) { + trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime); + trackData.tripStatisticsUpdater.updateTime(trackData.importTime); + } + trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics()); + trackData.track.setNumberOfPoints(trackData.numberOfLocations); + myTracksProviderUtils.updateTrack(trackData.track); + insertFirstWaypoint(trackData.track); + } + + /** + * On track segment start. + */ + protected void onTrackSegmentStart() { + trackData.numberOfSegments++; + + /* + * If not the first segment, add a pause separator if there is at least one + * location in the last segment. + */ + if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) { + insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0, + trackData.lastLocationInCurrentSegment.getTime())); + } + trackData.lastLocationInCurrentSegment = null; + } + + /** + * Adds a waypoint. + * + * @param type the waypoint type + */ + protected void addWaypoint(WaypointType type) throws SAXException { + // Waypoint must have a time, else cannot match to the track points + if (time == null) { + return; + } + + Waypoint waypoint = new Waypoint(); + Location location = createLocation(); + + if (!LocationUtils.isValidLocation(location)) { + throw new SAXException(createErrorMessage("Invalid location detected: " + location)); + } + waypoint.setLocation(location); + + if (name != null) { + waypoint.setName(name); + } + if (description != null) { + waypoint.setDescription(description); + } + if (category != null) { + waypoint.setCategory(category); + } + waypoint.setType(type); + waypoints.add(waypoint); + } + + /** + * Gets a track point. + */ + protected Location getTrackPoint() throws SAXException { + Location location = createLocation(); + + // Calculate derived attributes from the previous point + if (trackData.lastLocationInCurrentSegment != null + && trackData.lastLocationInCurrentSegment.getTime() != 0) { + long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime(); + + // Check for negative time change + if (timeDifference <= 0) { + Log.w(Constants.TAG, "Time difference not postive."); + } else { + + /* + * We don't have a speed and bearing in GPX, make something up from the + * last two points. 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. + */ + float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f + / timeDifference; + location.setSpeed(speed); + } + location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location)); + } + + if (!LocationUtils.isValidLocation(location)) { + throw new SAXException(createErrorMessage("Invalid location detected: " + location)); + } + + if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) { + /* + * If not the first segment, add a resume separator before adding the + * first location. + */ + insertLocation( + createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime())); + } + trackData.lastLocationInCurrentSegment = location; + return location; + } + + /** + * Inserts a track point. + * + * @param location the location + */ + protected void insertTrackPoint(Location location) { + insertLocation(location); + + if (trackData.track.getStartId() == -1L) { + // Flush the location to set the track start id and the track end id + flushLocations(trackData); + } + } + + /** + * Creates an error message. + * + * @param message the message + */ + protected String createErrorMessage(String message) { + return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", + locator.getLineNumber(), locator.getColumnNumber(), message); + } + + /** + * Creates a location. + */ + private Location createLocation() throws SAXException { + if (latitude == null || longitude == null || altitude == null) { + return null; + } + double longitudeValue; + double latitudeValue; + double altitudeValue; + + try { + longitudeValue = Double.parseDouble(longitude); + latitudeValue = Double.parseDouble(latitude); + altitudeValue = Double.parseDouble(altitude); + } catch (NumberFormatException e) { + throw new SAXException(createErrorMessage(String.format( + "Unable to parse longitude latitude altitude: %s %s %s", longitude, latitude, altitude)), + e); + } + long timeValue; + + if (time == null) { + timeValue = trackData.importTime; + } else { + try { + timeValue = StringUtils.getTime(time); + } catch (IllegalArgumentException e) { + throw new SAXException( + createErrorMessage(String.format("Unable to parse time: %s", time)), e); + } + } + return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue); + } + + /** + * Creates a location. + * + * @param latitudeValue the latitude value + * @param longitudeValue the longitude value + * @param altitudeValue the altitude value + * @param timeValue the time value + */ + private Location createLocation( + double latitudeValue, double longitudeValue, double altitudeValue, long timeValue) { + Location location = new Location(LocationManager.GPS_PROVIDER); + location.setLatitude(latitudeValue); + location.setLongitude(longitudeValue); + location.setAltitude(altitudeValue); + location.setTime(timeValue); + location.removeAccuracy(); + location.removeBearing(); + location.removeSpeed(); + return location; + } + + /** + * Inserts a location. + * + * @param location the location + */ + private void insertLocation(Location location) { + if (trackData.tripStatisticsUpdater == null) { + trackData.tripStatisticsUpdater = new TripStatisticsUpdater( + location.getTime() != -1L ? location.getTime() : trackData.importTime); + } + trackData.tripStatisticsUpdater.addLocation(location, minRecordingDistance); + + trackData.bufferedLocations[trackData.numBufferedLocations] = location; + trackData.numBufferedLocations++; + trackData.numberOfLocations++; + + if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) { + flushLocations(trackData); + } + } + + /** + * Flushes the locations to the database. + * + * @param data the track data + */ + private void flushLocations(TrackData data) { + if (data.numBufferedLocations <= 0) { + return; + } + myTracksProviderUtils.bulkInsertTrackPoint( + data.bufferedLocations, data.numBufferedLocations, data.track.getId()); + data.numBufferedLocations = 0; + if (data.track.getStartId() == -1L) { + data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId())); + } + data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId())); + } + + /** + * Inserts the first waypoint, the track statistics waypoint. + * + * @param track the track + */ + private void insertFirstWaypoint(Track track) { + String waypointName = context.getString(R.string.marker_split_name_format, 0); + String waypointCategory = ""; + TripStatisticsUpdater updater = new TripStatisticsUpdater( + track.getTripStatistics().getStartTime()); + TripStatistics tripStatistics = updater.getTripStatistics(); + String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription( + tripStatistics); + String icon = context.getString(R.string.marker_statistics_icon_url); + double length = 0.0; + long duration = 0L; + Location waypointLocation = new Location(""); + waypointLocation.setLatitude(100); + waypointLocation.setLongitude(180); + Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon, + track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation, + tripStatistics); + myTracksProviderUtils.insertWaypoint(waypoint); + } + + /** + * Handles an exception. + * + * @param e the exception + */ + private void handleException(Exception e) throws Exception { + for (long trackId : trackIds) { + myTracksProviderUtils.deleteTrack(trackId); + } + throw e; + } +} 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 0888e9041..c9eee1249 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 @@ -16,93 +16,26 @@ package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl; 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.content.Waypoint.WaypointType; -import com.google.android.apps.mytracks.services.TrackRecordingService; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.stats.TripStatisticsUpdater; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.TrackIconUtils; -import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; -import android.database.Cursor; import android.location.Location; -import android.location.LocationManager; -import android.net.Uri; -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -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; /** - * Imports GPX file to My Tracks. + * Imports a GPX file. * - * @author Leif Hendrik Wilden - * @author Steffen Horlacher - * @author Rodrigo Damazio + * @author Jimmy Shih */ -public class GpxImporter extends DefaultHandler { +public class GpxImporter extends AbstractImporter { - /** - * Data for the current track. - * - * @author Jimmy Shih - */ - private class TrackData { - - // The current track - Track track = new Track(); - - // The parsed depth for the current track - int parsedDepth = 0; - - // The number of segments processed for the current track - int numberOfSegments = 0; - - /* - * The last location in the current segment. Null if the current segment - * doesn't have a last location. - */ - Location lastLocationInCurrentSegment; - - // The number of locations processed for the current track - int numberOfLocations = 0; - - // The trip statistics updater for the current track - TripStatisticsUpdater tripStatisticsUpdater; - - // The default location time for the current track. -1L to not use it. - long defaultLocationTime = -1L; - - // The buffered locations - Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS]; - - // The number of buffered locations - int numBufferedLocations = 0; - } - - // GPX tags - private static final String TAG_ALTITUDE = "ele"; private static final String TAG_DESCRIPTION = "desc"; private static final String TAG_COMMENT = "cmt"; + private static final String TAG_ELEVATION = "ele"; private static final String TAG_GPX = "gpx"; private static final String TAG_NAME = "name"; private static final String TAG_TIME = "time"; @@ -112,150 +45,65 @@ public class GpxImporter extends DefaultHandler { private static final String TAG_TYPE = "type"; private static final String TAG_WAYPOINT = "wpt"; - // GPX attributes - private static final String ATT_LAT = "lat"; - private static final String ATT_LON = "lon"; + private static final String ATTRIBUTE_LAT = "lat"; + private static final String ATTRIBUTE_LON = "lon"; - // The maximum number of buffered locations for bulk-insertion - private static final int MAX_BUFFERED_LOCATIONS = 512; - - private final Context context; - private final MyTracksProviderUtils myTracksProviderUtils; - private final int minRecordingDistance; - private final List tracksIds; - - // The SAX locator to get the current line information - private Locator locator; - - // The current element content - private String content; - - // The current track data - private TrackData trackData = null; - - // The current waypoint - private Waypoint currentWaypoint; - - // The list of waypoints - private List waypoints = new ArrayList(); - - /* - * The current location, used for both track point location and waypoint - * location. - */ - private Location location; - - /** - * Reads GPS tracks from a GPX file and writes tracks, waypoints, and track - * points to the database. - * - * @param context the context - * @param inputStream the input stream for the GPX file - * @param myTracksProviderUtils my tracks provider utils - * @param minRecordingDistance the min recording distance - * @return long[] array of track ids written to the database. - */ - public static long[] importGPXFile(Context context, InputStream inputStream, - MyTracksProviderUtils myTracksProviderUtils, int minRecordingDistance) - throws ParserConfigurationException, SAXException, IOException { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - SAXParser saxParser = saxParserFactory.newSAXParser(); - GpxImporter gpxImporter = new GpxImporter(context, myTracksProviderUtils, minRecordingDistance); - long[] trackIds = new long[0]; - - try { - long start = System.currentTimeMillis(); - - saxParser.parse(inputStream, gpxImporter); - - long end = System.currentTimeMillis(); - Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms"); - - trackIds = gpxImporter.getImportedTrackIds(); - if (trackIds.length == 0) { - throw new IOException("No track imported."); - } - } finally { - // Delete the current track if not finished - gpxImporter.rollbackUnfinishedTracks(); - } - return trackIds; + public GpxImporter(Context context) { + super(context); } - public GpxImporter( - Context context, MyTracksProviderUtils myTracksProviderUtils, int minRecordingDistance) { - this.context = context; - this.myTracksProviderUtils = myTracksProviderUtils; - this.minRecordingDistance = minRecordingDistance; - this.tracksIds = new ArrayList(); + @VisibleForTesting + public GpxImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) { + super(context, myTracksProviderUtils); } @Override - public void characters(char[] ch, int start, int length) throws SAXException { - String newContent = new String(ch, start, length); - if (content == null) { - content = newContent; - } else { - /* - * In 99% of the cases, a single call to this method will be made for each - * sequence of characters we're interested in, so we'll rarely be - * concatenating strings, thus not justifying the use of a StringBuilder. - */ - content += newContent; - } - } - - @Override - public void startElement(String uri, String localName, String name, Attributes attributes) + public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { - if (trackData != null) { - trackData.parsedDepth++; - if (localName.equals(TAG_TRACK)) { - throw new SAXException(createErrorMessage("Invalid GPX. Already inside a track.")); - } else if (localName.equals(TAG_TRACK_SEGMENT)) { - onTrackSegmentElementStart(); - } else if (localName.equals(TAG_TRACK_POINT)) { - onTrackPointElementStart(attributes); - } - } else { - if (localName.equals(TAG_TRACK)) { - onTrackElementStart(); - } else if (localName.equals(TAG_WAYPOINT)) { - onWaypointElementStart(attributes); - } + if (tag.equals(TAG_WAYPOINT)) { + onWaypointStart(attributes); + } else if (tag.equals(TAG_TRACK)) { + onTrackStart(); + } else if (tag.equals(TAG_TRACK_SEGMENT)) { + onTrackSegmentStart(); + } else if (tag.equals(TAG_TRACK_POINT)) { + onTrackPointStart(attributes); } } @Override - public void endElement(String uri, String localName, String name) throws SAXException { - if (localName.equals(TAG_NAME)) { - onNameElementEnd(); - } else if (localName.equals(TAG_DESCRIPTION)) { - onDescriptionElementEnd(); - } else if (localName.equals(TAG_TYPE)) { - onTypeElementEnd(); - } else if (localName.equals(TAG_ALTITUDE)) { - onAltitudeElementEnd(); - } else if (localName.equals(TAG_TIME)) { - onTimeElementEnd(); - } else if (localName.equals(TAG_GPX)) { - onGpxElementEnd(); - } - - if (trackData != null) { - trackData.parsedDepth--; - if (localName.equals(TAG_TRACK)) { - onTrackElementEnd(); - } else if (localName.equals(TAG_TRACK_SEGMENT)) { - onTrackSegmentElementEnd(); - } else if (localName.equals(TAG_TRACK_POINT)) { - onTrackPointElementEnd(); + public void endElement(String uri, String localName, String tag) throws SAXException { + if (tag.equals(TAG_GPX)) { + onFileEnd(); + } else if (tag.equals(TAG_WAYPOINT)) { + onWaypointEnd(); + } else if (tag.equals(TAG_TRACK)) { + onTrackEnd(); + } else if (tag.equals(TAG_TRACK_POINT)) { + onTrackPointEnd(); + } else if (tag.equals(TAG_NAME)) { + if (content != null) { + name = content.trim(); } - } else { - if (localName.equals(TAG_WAYPOINT)) { - onWaypointElementEnd(); - } else if (localName.equals(TAG_COMMENT)) { - onCommentElementEnd(); + } else if (tag.equals(TAG_DESCRIPTION)) { + if (content != null) { + description = content.trim(); + } + } else if (tag.equals(TAG_TYPE)) { + if (content != null) { + category = content.trim(); + } + } else if (tag.equals(TAG_TIME)) { + if (content != null) { + time = content.trim(); + } + } else if (tag.equals(TAG_ELEVATION)) { + if (content != null) { + altitude = content.trim(); + } + } else if (tag.equals(TAG_COMMENT)) { + if (content != null) { + waypointType = content.trim(); } } @@ -264,486 +112,57 @@ public class GpxImporter extends DefaultHandler { } @Override - public void setDocumentLocator(Locator locator) { - this.locator = locator; + protected void onTrackStart() { + super.onTrackStart(); + name = null; + description = null; + category = null; } /** - * Rolls back last track if possible. - */ - public void rollbackUnfinishedTracks() { - if (trackData != null) { - myTracksProviderUtils.deleteTrack(trackData.track.getId()); - trackData = null; - } - } - - /** - * On gpx element end. - */ - private void onGpxElementEnd() { - int size = tracksIds.size(); - if (size > 0) { - long trackId = tracksIds.get(size - 1); - Track track = myTracksProviderUtils.getTrack(trackId); - if (track != null) { - addWaypoints(track); - } - } - } - - /** - * On track element start. - */ - private void onTrackElementStart() { - trackData = new TrackData(); - Uri uri = myTracksProviderUtils.insertTrack(trackData.track); - trackData.track.setId(Long.parseLong(uri.getLastPathSegment())); - } - - /** - * On track element end. - */ - private void onTrackElementEnd() { - flushPoints(trackData); - if (trackData.tripStatisticsUpdater == null) { - long now = System.currentTimeMillis(); - trackData.tripStatisticsUpdater = new TripStatisticsUpdater(now); - trackData.tripStatisticsUpdater.updateTime(now); - } - trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics()); - trackData.track.setNumberOfPoints(trackData.numberOfLocations); - myTracksProviderUtils.updateTrack(trackData.track); - tracksIds.add(trackData.track.getId()); - insertFirstWaypoint(trackData.track); - trackData = null; - } - - /** - * On track segment start. - */ - private void onTrackSegmentElementStart() { - trackData.numberOfSegments++; - - /* - * If not the first segment, add a pause separator if there is at least one - * location in the last segment. - */ - if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) { - insertPoint(createNewLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, - trackData.lastLocationInCurrentSegment.getTime())); - } - trackData.lastLocationInCurrentSegment = null; - } - - /** - * On track segment element end. - */ - private void onTrackSegmentElementEnd() { - // Nothing needs to be done - } - - /** - * On track point element start. + * On track point start. * * @param attributes the attributes */ - private void onTrackPointElementStart(Attributes attributes) throws SAXException { - parseLocation(attributes); + private void onTrackPointStart(Attributes attributes) { + latitude = attributes.getValue(ATTRIBUTE_LAT); + longitude = attributes.getValue(ATTRIBUTE_LON); + altitude = DEFAULT_ALTITUDE; + time = null; } /** - * On track point element end. + * On track point end. */ - private void onTrackPointElementEnd() throws SAXException { - if (!LocationUtils.isValidLocation(location)) { - throw new SAXException(createErrorMessage("Invalid location detected: " + location)); + private void onTrackPointEnd() throws SAXException { + Location location = getTrackPoint(); + if (location == null) { + return; } - - if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) { - /* - * If not the first segment, add a resume separator before adding the - * first location. - */ - insertPoint( - createNewLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, location.getTime())); - } - - insertPoint(location); - - if (trackData.track.getStartId() == -1L) { - // Flush the location to set the track start id and the track end id - flushPoints(trackData); - } - - trackData.lastLocationInCurrentSegment = location; - location = null; + insertTrackPoint(location); } /** - * On waypoint element start. + * On waypoint start. * * @param attributes the attributes */ - private void onWaypointElementStart(Attributes attributes) throws SAXException { - if (currentWaypoint != null) { - throw new SAXException(createErrorMessage("Found waypoint inside another waypoint.")); - } - parseLocation(attributes); - currentWaypoint = new Waypoint(); + private void onWaypointStart(Attributes attributes) { + name = null; + description = null; + category = null; + latitude = attributes.getValue(ATTRIBUTE_LAT); + longitude = attributes.getValue(ATTRIBUTE_LON); + altitude = DEFAULT_ALTITUDE; + time = null; + waypointType = null; } /** - * On waypoint element end. + * On waypoint end. */ - private void onWaypointElementEnd() throws SAXException { - if (!LocationUtils.isValidLocation(location)) { - throw new SAXException(createErrorMessage("Invalid location detected: " + location)); - } - currentWaypoint.setLocation(location); - waypoints.add(currentWaypoint); - location = null; - currentWaypoint = null; - } - - /** - * On name element end. - */ - private void onNameElementEnd() { - if (content != null) { - String name = content.toString().trim(); - if (trackData != null && trackData.parsedDepth == 1) { - trackData.track.setName(name); - } else if (currentWaypoint != null) { - currentWaypoint.setName(name); - } - } - } - - /** - * On description element end. - */ - private void onDescriptionElementEnd() { - if (content != null) { - String description = content.toString().trim(); - if (trackData != null && trackData.parsedDepth == 1) { - trackData.track.setDescription(description); - } else if (currentWaypoint != null) { - currentWaypoint.setDescription(description); - } - } - } - - /** - * On type element end. - */ - private void onTypeElementEnd() { - if (content != null) { - String type = content.toString().trim(); - if (trackData != null && trackData.parsedDepth == 1) { - trackData.track.setCategory(type); - trackData.track.setIcon(TrackIconUtils.getIconValue(context, type)); - } else if (currentWaypoint != null) { - currentWaypoint.setCategory(type); - } - } - } - - /** - * On altitude element end. - */ - private void onAltitudeElementEnd() throws SAXException { - if (location == null || content == null) { - return; - } - - try { - location.setAltitude(Double.parseDouble(content)); - } catch (NumberFormatException e) { - throw new SAXException(createErrorMessage("Unable to parse altitude: " + content), e); - } - } - - /** - * On time element end. Sets location time and doing additional calculations - * as this is the last value required for the location. Also sets the start - * time for the trip statistics builder as there is no start time in the track - * root element. - */ - private void onTimeElementEnd() throws SAXException { - if (location == null || content == null) { - return; - } - - // Parse the time - long time; - try { - time = StringUtils.getTime(content.trim()); - } catch (IllegalArgumentException e) { - throw new SAXException(createErrorMessage("Unable to parse time: " + content), e); - } - location.setTime(time); - - // Calculate derived attributes from the previous point - if (trackData != null && trackData.lastLocationInCurrentSegment != null - && trackData.lastLocationInCurrentSegment.getTime() != 0) { - long timeDifference = time - trackData.lastLocationInCurrentSegment.getTime(); - - // Check for negative time change - if (timeDifference <= 0) { - Log.w(Constants.TAG, "Time difference not postive."); - } else { - - /* - * We don't have a speed and bearing in GPX, make something up from the - * last two points. 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. - */ - float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f - / timeDifference; - location.setSpeed(speed); - } - location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location)); - } - } - - /** - * On comment element end. - */ - private void onCommentElementEnd() { - if (content != null) { - String comment = content.toString().trim(); - if (currentWaypoint != null) { - WaypointType waypointType = WaypointType.STATISTICS.name().equals(comment) ? WaypointType.STATISTICS - : WaypointType.WAYPOINT; - currentWaypoint.setType(waypointType); - - } - } - } - - /** - * Parses a location. - * - * @param attributes the attributes - */ - private void parseLocation(Attributes attributes) throws SAXException { - if (location != null) { - throw new SAXException(createErrorMessage("Found a location inside another one.")); - } - String latitude = attributes.getValue(ATT_LAT); - String longitude = attributes.getValue(ATT_LON); - - if (latitude == null || longitude == null) { - throw new SAXException(createErrorMessage("Point with no longitude or latitude.")); - } - double latitudeValue; - double longitudeValue; - try { - latitudeValue = Double.parseDouble(latitude); - longitudeValue = Double.parseDouble(longitude); - } catch (NumberFormatException e) { - throw new SAXException( - createErrorMessage("Unable to parse latitude/longitude: " + latitude + "/" + longitude), - e); - } - location = createNewLocation(latitudeValue, longitudeValue, -1L); - } - - /** - * Creates a new location - * - * @param latitude location latitude - * @param longitude location longitude - * @param time location time - */ - private Location createNewLocation(double latitude, double longitude, long time) { - Location newLocation = new Location(LocationManager.GPS_PROVIDER); - newLocation.setLatitude(latitude); - newLocation.setLongitude(longitude); - newLocation.setAltitude(0.0f); - newLocation.setTime(time); - newLocation.removeAccuracy(); - newLocation.removeBearing(); - newLocation.removeSpeed(); - return newLocation; - } - - /** - * Inserts a point. - * - * @param newLocation the location - */ - private void insertPoint(Location newLocation) { - if (trackData.tripStatisticsUpdater == null) { - // Set the default location time if the newLocation does not have a time - trackData.defaultLocationTime = newLocation.getTime() == -1L ? System.currentTimeMillis() - : -1L; - trackData.tripStatisticsUpdater = new TripStatisticsUpdater( - newLocation.getTime() != -1L ? newLocation.getTime() : trackData.defaultLocationTime); - } - if (trackData.defaultLocationTime != -1L) { - newLocation.setTime(trackData.defaultLocationTime); - } - trackData.tripStatisticsUpdater.addLocation(newLocation, minRecordingDistance); - - trackData.bufferedLocations[trackData.numBufferedLocations] = newLocation; - trackData.numBufferedLocations++; - trackData.numberOfLocations++; - - if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) { - flushPoints(trackData); - } - } - - /** - * Flushes the points to the database. - * - * @param data the track data - */ - private void flushPoints(TrackData data) { - if (data.numBufferedLocations <= 0) { - return; - } - myTracksProviderUtils.bulkInsertTrackPoint( - data.bufferedLocations, data.numBufferedLocations, data.track.getId()); - data.numBufferedLocations = 0; - if (data.track.getStartId() == -1L) { - data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId())); - } - data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId())); - } - - /** - * Gets the imported track ids. - */ - private long[] getImportedTrackIds() { - long[] result = new long[tracksIds.size()]; - for (int i = 0; i < result.length; i++) { - result[i] = tracksIds.get(i); - } - return result; - } - - /** - * Creates an error message. - * - * @param message the message - */ - private String createErrorMessage(String message) { - return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", - locator.getLineNumber(), locator.getColumnNumber(), message); - } - - /** - * Inserts the first track waypoint, the track statistics waypoint. - * - * @param track the track - */ - private void insertFirstWaypoint(Track track) { - String name = context.getString(R.string.marker_split_name_format, 0); - String category = ""; - TripStatisticsUpdater updater = new TripStatisticsUpdater( - track.getTripStatistics().getStartTime()); - TripStatistics tripStatistics = updater.getTripStatistics(); - String description = new DescriptionGeneratorImpl(context).generateWaypointDescription( - tripStatistics); - String icon = context.getString(R.string.marker_statistics_icon_url); - double length = 0.0; - long duration = 0L; - Location waypointLocation = new Location(""); - waypointLocation.setLatitude(100); - waypointLocation.setLongitude(180); - Waypoint waypoint = new Waypoint(name, description, category, icon, track.getId(), - WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation, tripStatistics); - myTracksProviderUtils.insertWaypoint(waypoint); - } - - /** - * Adds waypoints to a track. - * - * @param track the track - */ - private void addWaypoints(Track track) { - Cursor trackPointCursor = null; - try { - trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false); - if (trackPointCursor == null) { - return; - } - int waypointPosition = -1; - Waypoint waypoint = null; - int trackPointPosition = -1; - Location trackPoint = null; - TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater( - track.getTripStatistics().getStartTime()); - TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater( - track.getTripStatistics().getStartTime()); - - while (true) { - if (waypoint == null) { - waypointPosition++; - waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null; - if (waypoint == null) { - // No more waypoints - return; - } - } - if (trackPoint == null) { - trackPointPosition++; - trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils - .createTrackPoint(trackPointCursor) - : null; - if (trackPoint == null) { - // No more track points. Ignore the rest of the waypoints. - return; - } - trackTripStatisticstrackUpdater.addLocation(trackPoint, minRecordingDistance); - markerTripStatisticsUpdater.addLocation(trackPoint, minRecordingDistance); - } - if (waypoint.getLocation().getTime() > trackPoint.getTime()) { - trackPoint = null; - } else if (waypoint.getLocation().getTime() < trackPoint.getTime()) { - waypoint = null; - } else { - // The waypoint location time matches the track point time - if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude() - && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) { - - // Get tripStatistics, description, and icon - TripStatistics tripStatistics; - String description; - String icon; - if (waypoint.getType() == WaypointType.STATISTICS) { - tripStatistics = markerTripStatisticsUpdater.getTripStatistics(); - markerTripStatisticsUpdater = new TripStatisticsUpdater(trackPoint.getTime()); - description = new DescriptionGeneratorImpl(context).generateWaypointDescription( - tripStatistics); - icon = context.getString(R.string.marker_statistics_icon_url); - } else { - tripStatistics = null; - description = waypoint.getDescription(); - icon = context.getString(R.string.marker_waypoint_icon_url); - } - - // Get length and duration - double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance(); - long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime(); - - // Insert waypoint - Waypoint newWaypoint = new Waypoint(waypoint.getName(), description, - waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration, - -1L, -1L, trackPoint, tripStatistics); - myTracksProviderUtils.insertWaypoint(newWaypoint); - } - waypoint = null; - } - } - } finally { - if (trackPointCursor != null) { - trackPointCursor.close(); - } - } + private void onWaypointEnd() throws SAXException { + addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS + : WaypointType.WAYPOINT); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java index e9b747c72..2cae4923c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java @@ -35,36 +35,39 @@ import android.support.v4.app.TaskStackBuilder; import android.util.Log; import android.widget.Toast; +import java.io.File; + /** - * An activity to import GPX files from the SD card. Optionally to import one - * GPX file and display it in My Tracks. - * + * An activity to import files from the external storage. Optionally to import + * one specific file. + * * @author Rodrigo Damazio */ public class ImportActivity extends Activity { public static final String EXTRA_IMPORT_ALL = "import_all"; + public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format"; private static final String TAG = ImportActivity.class.getSimpleName(); - private static final int DIALOG_PROGRESS_ID = 0; private static final int DIALOG_RESULT_ID = 1; private ImportAsyncTask importAsyncTask; private ProgressDialog progressDialog; - + private boolean importAll; - - // path on the SD card to import + private TrackFileFormat trackFileFormat; + + // the path on the external storage to import private String path; - // number of succesfully imported files + // the number of successfully imported files private int successCount; - - // number of files to import + + // the number of files to import private int totalCount; - - // last successfully imported track id + + // the last successfully imported track id private long trackId; @Override @@ -73,8 +76,25 @@ public class ImportActivity extends Activity { Intent intent = getIntent(); importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false); + trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT); + if (trackFileFormat == null) { + trackFileFormat = TrackFileFormat.GPX; + } + + if (!FileUtils.isExternalStorageAvailable()) { + Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show(); + finish(); + return; + } if (importAll) { - path = FileUtils.buildExternalDirectoryPath("gpx"); + path = FileUtils.buildExternalDirectoryPath( + trackFileFormat == TrackFileFormat.KML ? "kml" : "gpx"); + if (!FileUtils.isDirectory(new File(path))) { + Toast.makeText(this, getString(R.string.import_no_directory, path), Toast.LENGTH_LONG) + .show(); + finish(); + return; + } } else { String action = intent.getAction(); if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) { @@ -97,7 +117,7 @@ public class ImportActivity extends Activity { importAsyncTask = (ImportAsyncTask) retained; importAsyncTask.setActivity(this); } else { - importAsyncTask = new ImportAsyncTask(this, importAll, path); + importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, path); importAsyncTask.execute(); } } @@ -112,41 +132,36 @@ public class ImportActivity extends Activity { protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_PROGRESS_ID: - progressDialog = DialogUtils.createHorizontalProgressDialog(this, - R.string.external_storage_import_progress_message, - new DialogInterface.OnCancelListener() { + progressDialog = DialogUtils.createHorizontalProgressDialog( + this, R.string.import_progress_message, new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { importAsyncTask.cancel(true); finish(); } - }); + }, path); return progressDialog; case DIALOG_RESULT_ID: String message; - if (successCount == 0) { - message = getString(R.string.external_storage_import_error_no_file, path); + if (totalCount == 0) { + message = getString(R.string.import_no_file, path); } else { String totalFiles = getResources() - .getQuantityString(R.plurals.importGpxFiles, totalCount, totalCount); - message = getString( - R.string.external_storage_import_success_count, successCount, totalFiles, path); + .getQuantityString(R.plurals.importFiles, totalCount, totalCount); + message = getString(R.string.import_success, successCount, totalFiles, path); } - return new AlertDialog.Builder(this) - .setCancelable(true) - .setMessage(message) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override + return new AlertDialog.Builder(this).setCancelable(true) + .setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override public void onCancel(DialogInterface dialog) { finish(); } - }) - .setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { - @Override + }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { if (!importAll && trackId != -1L) { - Intent intent = IntentUtils - .newIntent(ImportActivity.this, TrackDetailActivity.class) + Intent intent = IntentUtils.newIntent( + ImportActivity.this, TrackDetailActivity.class) .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(ImportActivity.this); taskStackBuilder.addNextIntent(intent); @@ -154,8 +169,7 @@ public class ImportActivity extends Activity { } finish(); } - }) - .create(); + }).create(); default: return null; } @@ -163,23 +177,17 @@ public class ImportActivity extends Activity { /** * Invokes when the associated AsyncTask completes. - * - * @param success true if the AsyncTask is successful - * @param imported the number of files successfully imported - * @param total the total number of files to import - * @param id the last successfully imported track id + * + * @param aSuccessCount the number of files successfully imported + * @param aTotalCount the total number of files to import + * @param aTrackId the last successfully imported track id */ - public void onAsyncTaskCompleted(boolean success, int imported, int total, long id) { - successCount = imported; - totalCount = total; - trackId = id; + public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) { + successCount = aSuccessCount; + totalCount = aTotalCount; + trackId = aTrackId; removeDialog(DIALOG_PROGRESS_ID); - if (success) { - showDialog(DIALOG_RESULT_ID); - } else { - Toast.makeText(this, R.string.external_storage_import_error, Toast.LENGTH_LONG).show(); - finish(); - } + showDialog(DIALOG_RESULT_ID); } /** @@ -191,7 +199,7 @@ public class ImportActivity extends Activity { /** * Sets the progress dialog value. - * + * * @param number the number of files imported * @param max the maximum number of files */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java index 0b714b4d7..2e2cba2dc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java @@ -16,8 +16,6 @@ package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; @@ -28,18 +26,12 @@ import android.util.Log; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - /** - * AsyncTask to import GPX files from the SD card. - * + * AsyncTask to import files from the external storage. + * * @author Jimmy Shih */ public class ImportAsyncTask extends AsyncTask { @@ -48,20 +40,17 @@ public class ImportAsyncTask extends AsyncTask { private ImportActivity importActivity; private final boolean importAll; + private final TrackFileFormat trackFileFormat; private final String path; - private final MyTracksProviderUtils myTracksProviderUtils; private WakeLock wakeLock; - // true if the AsyncTask result is success - private boolean success; - // true if the AsyncTask has completed private boolean completed; - // number of files successfully imported + // the number of files successfully imported private int successCount; - // number of files to import + // the number of files to import private int totalCount; // the last successfully imported track id @@ -69,18 +58,19 @@ public class ImportAsyncTask extends AsyncTask { /** * Creates an AsyncTask. - * + * * @param importActivity the activity currently associated with this AsyncTask * @param importAll true to import all GPX files + * @param trackFileFormat the track file format * @param path path to import GPX files */ - public ImportAsyncTask(ImportActivity importActivity, boolean importAll, String path) { + public ImportAsyncTask(ImportActivity importActivity, boolean importAll, + TrackFileFormat trackFileFormat, String path) { this.importActivity = importActivity; this.importAll = importAll; + this.trackFileFormat = trackFileFormat; this.path = path; - myTracksProviderUtils = MyTracksProviderUtils.Factory.get(importActivity); - // Get the wake lock if not recording or paused if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key) == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT || PreferencesUtils.getBoolean( @@ -89,7 +79,6 @@ public class ImportAsyncTask extends AsyncTask { wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock); } - success = false; completed = false; successCount = 0; totalCount = 0; @@ -98,13 +87,13 @@ public class ImportAsyncTask extends AsyncTask { /** * Sets the current {@link ImportActivity} associated with this AyncTask. - * + * * @param importActivity the current {@link ImportActivity}, can be null */ public void setActivity(ImportActivity importActivity) { this.importActivity = importActivity; if (completed && importActivity != null) { - importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId); + importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId); } } @@ -118,10 +107,6 @@ public class ImportAsyncTask extends AsyncTask { @Override protected Boolean doInBackground(Void... params) { try { - if (!FileUtils.isSdCardAvailable()) { - return false; - } - List files = getFiles(); totalCount = files.size(); if (totalCount == 0) { @@ -133,8 +118,7 @@ public class ImportAsyncTask extends AsyncTask { // If cancelled, return true to show the number of files imported return true; } - File file = files.get(i); - if (importFile(file)) { + if (importFile(files.get(i))) { successCount++; } publishProgress(i + 1, totalCount); @@ -157,46 +141,36 @@ public class ImportAsyncTask extends AsyncTask { @Override protected void onPostExecute(Boolean result) { - success = result; completed = true; if (importActivity != null) { - importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId); + importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId); } } /** - * Imports a GPX file. - * + * Imports a file. + * * @param file the file */ private boolean importFile(final File file) { try { - int minRecordingDistance = PreferencesUtils.getInt(importActivity, - R.string.min_recording_distance_key, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); - long trackIds[] = GpxImporter.importGPXFile(importActivity, - new FileInputStream(file), myTracksProviderUtils, minRecordingDistance); + AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter( + importActivity) + : new GpxImporter(importActivity); + long trackIds[] = importer.importFile(new FileInputStream(file)); int length = trackIds.length; if (length > 0) { trackId = trackIds[length - 1]; } return true; - } catch (FileNotFoundException e) { - Log.d(TAG, "file: " + file.getAbsolutePath(), e); - return false; - } catch (ParserConfigurationException e) { - Log.d(TAG, "file: " + file.getAbsolutePath(), e); - return false; - } catch (SAXException e) { - Log.d(TAG, "file: " + file.getAbsolutePath(), e); - return false; - } catch (IOException e) { + } catch (Exception e) { Log.d(TAG, "file: " + file.getAbsolutePath(), e); return false; } } /** - * Gets a list of GPX files. If importAll is true, returns a list of GPX files + * Gets a list of files. If importAll is true, returns a list of the files * under the path directory. If importAll is false, returns a list containing * just the path file. */ @@ -207,7 +181,8 @@ public class ImportAsyncTask extends AsyncTask { File[] candidates = file.listFiles(); if (candidates != null) { for (File candidate : candidates) { - if (!candidate.isDirectory() && candidate.getName().endsWith(".gpx")) { + if (!candidate.isDirectory() && candidate.getName() + .endsWith(trackFileFormat == TrackFileFormat.KML ? ".kml" : ".gpx")) { files.add(candidate); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java new file mode 100644 index 000000000..e9a74222f --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java @@ -0,0 +1,280 @@ +/* + * 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.file; + +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.content.Waypoint.WaypointType; +import com.google.common.annotations.VisibleForTesting; + +import android.content.Context; +import android.location.Location; + +import java.util.ArrayList; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * Imports a KML file. + * + * @author Jimmy Shih + */ +public class KmlImporter extends AbstractImporter { + + private static final String CADENCE = "cadence"; + private static final String HEART_RATE = "heart_rate"; + private static final String POWER = "power"; + + private static final String STATISTICS_STYLE = "#statistics"; + private static final String WAYPOINT_STYLE = "#waypoint"; + + private static final String TAG_COORDINATES = "coordinates"; + private static final String TAG_DESCRIPTION = "description"; + private static final String TAG_GX_COORD = "gx:coord"; + private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack"; + private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData"; + private static final String TAG_GX_TRACK = "gx:Track"; + private static final String TAG_GX_VALUE = "gx:value"; + private static final String TAG_KML = "kml"; + private static final String TAG_NAME = "name"; + private static final String TAG_PLACEMARK = "Placemark"; + private static final String TAG_STYLE_URL = "styleUrl"; + private static final String TAG_VALUE = "value"; + private static final String TAG_WHEN = "when"; + + private static final String ATTRIBUTE_NAME = "name"; + + private String sensorName; + private ArrayList locationList; + private ArrayList cadenceList; + private ArrayList heartRateList; + private ArrayList powerList; + + public KmlImporter(Context context) { + super(context); + } + + @VisibleForTesting + public KmlImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) { + super(context, myTracksProviderUtils); + } + + @Override + public void startElement(String uri, String localName, String tag, Attributes attributes) + throws SAXException { + if (tag.equals(TAG_PLACEMARK)) { + onWaypointStart(); + } else if (tag.equals(TAG_GX_MULTI_TRACK)) { + onTrackStart(); + } else if (tag.equals(TAG_GX_TRACK)) { + onTrackSegmentStart(); + } else if (tag.equals(TAG_GX_SIMPLE_ARRAY_DATA)) { + onSensorDataStart(attributes); + } + } + + @Override + public void endElement(String uri, String localName, String tag) throws SAXException { + if (tag.equals(TAG_KML)) { + onFileEnd(); + } else if (tag.equals(TAG_PLACEMARK)) { + onWaypointEnd(); + } else if (localName.equals(TAG_COORDINATES)) { + onWaypointLocationEnd(); + } else if (tag.equals(TAG_GX_MULTI_TRACK)) { + onTrackEnd(); + } else if (tag.equals(TAG_GX_TRACK)) { + onTrackSegmentEnd(); + } else if (tag.equals(TAG_GX_COORD)) { + onTrackPointEnd(); + } else if (tag.equals(TAG_GX_VALUE)) { + onSensorValueEnd(); + } else if (tag.equals(TAG_NAME)) { + if (content != null) { + name = content.trim(); + } + } else if (localName.equals(TAG_DESCRIPTION)) { + if (content != null) { + description = content.trim(); + } + } else if (localName.equals(TAG_VALUE)) { + if (content != null) { + category = content.trim(); + } + } else if (localName.equals(TAG_WHEN)) { + if (content != null) { + time = content.trim(); + } + } else if (localName.equals(TAG_STYLE_URL)) { + if (content != null) { + waypointType = content.trim(); + } + } + + // Reset element content + content = null; + } + + /** + * On waypoint start. + */ + private void onWaypointStart() { + // Reset all Placemark variables + name = null; + description = null; + category = null; + latitude = null; + longitude = null; + altitude = DEFAULT_ALTITUDE; + time = null; + waypointType = null; + } + + /** + * On waypoint end. + */ + private void onWaypointEnd() throws SAXException { + // Add a waypoint if the waypointType matches + WaypointType type = null; + if (WAYPOINT_STYLE.equals(waypointType)) { + type = WaypointType.WAYPOINT; + } else if (STATISTICS_STYLE.equals(waypointType)) { + type = WaypointType.STATISTICS; + } + if (type == null) { + return; + } + addWaypoint(type); + } + + /** + * On waypoint location end. + */ + private void onWaypointLocationEnd() { + if (content != null) { + String parts[] = content.trim().split(","); + if (parts.length != 2 && parts.length != 3) { + return; + } + longitude = parts[0]; + latitude = parts[1]; + if (parts.length == 3) { + altitude = parts[2]; + } + } + } + + @Override + protected void onTrackSegmentStart() { + super.onTrackSegmentStart(); + locationList = new ArrayList(); + powerList = new ArrayList(); + cadenceList = new ArrayList(); + heartRateList = new ArrayList(); + } + + /** + * On track segment end. + */ + private void onTrackSegmentEnd() { + // Close a track segment by inserting the segment locations + boolean hasPower = powerList.size() == locationList.size(); + boolean hasCadence = cadenceList.size() == locationList.size(); + boolean hasHeartRate = heartRateList.size() == locationList.size(); + + for (int i = 0; i < locationList.size(); i++) { + Location location = locationList.get(i); + + if (!hasPower && !hasCadence && !hasHeartRate) { + insertTrackPoint(location); + } else { + SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder(); + if (hasPower) { + builder.setPower(Sensor.SensorData.newBuilder() + .setValue(powerList.get(i)).setState(Sensor.SensorState.SENDING)); + } + if (hasCadence) { + builder.setCadence(Sensor.SensorData.newBuilder() + .setValue(cadenceList.get(i)).setState(Sensor.SensorState.SENDING)); + } + if (hasHeartRate) { + builder.setHeartRate(Sensor.SensorData.newBuilder() + .setValue(heartRateList.get(i)).setState(Sensor.SensorState.SENDING)); + } + SensorDataSet sensorDataSet = builder.setCreationTime(location.getTime()).build(); + MyTracksLocation myTracksLocation = new MyTracksLocation(location, sensorDataSet); + insertTrackPoint(myTracksLocation); + } + } + } + + /** + * On track point end. gx:coord end tag. + */ + private void onTrackPointEnd() throws SAXException { + // Add location to locationList + if (content == null) { + return; + } + String parts[] = content.trim().split(" "); + if (parts.length != 3) { + return; + } + longitude = parts[0]; + latitude = parts[1]; + altitude = parts[2]; + Location location = getTrackPoint(); + if (location == null) { + return; + } + locationList.add(location); + time = null; + } + + /** + * On sensor data start. gx:SimpleArrayData start tag. + * + * @param attributes + */ + private void onSensorDataStart(Attributes attributes) { + sensorName = attributes.getValue(ATTRIBUTE_NAME); + } + + /** + * On sensor value end. gx:value end tag. + */ + private void onSensorValueEnd() throws SAXException { + if (content != null) { + int value; + try { + value = Integer.parseInt(content.trim()); + } catch (NumberFormatException e) { + throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); + } + if (POWER.equals(sensorName)) { + powerList.add(value); + } else if (HEART_RATE.equals(sensorName)) { + heartRateList.add(value); + } else if (CADENCE.equals(sensorName)) { + cadenceList.add(value); + } + } + } +} 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 99fd29408..25e7f0539 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 @@ -303,6 +303,8 @@ public class KmlTrackWriter implements TrackFormatWriter { printWriter.println("" + StringUtils.formatCData(name) + ""); printWriter.println( "" + StringUtils.formatCData(description) + ""); + printWriter.println("" + + StringUtils.formatDateTimeIso8601(location.getTime()) + ""); printWriter.println("#" + styleName + ""); writeCategory(category); printWriter.println(""); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java index a97d07431..30c37782b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java @@ -194,9 +194,8 @@ class TrackWriterImpl implements TrackWriter { * Checks and returns whether we're ready to create the output file. */ protected boolean canWriteFile() { - if (!FileUtils.isSdCardAvailable()) { - Log.i(Constants.TAG, "Could not find SD card."); - errorMessage = R.string.external_storage_error_no_storage; + if (!FileUtils.isExternalStorageWriteable()) { + errorMessage = R.string.external_storage_not_writeable; return false; } 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 11d7ff5da..659552e65 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -37,29 +37,45 @@ public class FileUtils { */ @VisibleForTesting static final int MAX_FAT32_PATH_LENGTH = 260; - + /** - * Returns whether the SD card is available. + * Returns true if the external storage is available. */ - public static boolean isSdCardAvailable() { - return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + public static boolean isExternalStorageAvailable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state) + || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + } + + /** + * Returns true if the external storage is writable. + */ + public static boolean isExternalStorageWriteable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + /** + * Returns true if the directory exists. + * + * @param dir the directory + */ + public static boolean isDirectory(File dir) { + return dir.exists() && dir.isDirectory(); } /** - * Ensures the given directory exists by creating it and its parents if + * Ensures the directory exists by creating it and its parents if * necessary. * * @return whether the directory exists (either already existed or was * successfully created) */ public static boolean ensureDirectoryExists(File dir) { - if (dir.exists() && dir.isDirectory()) { + if (isDirectory(dir)) { return true; } - if (dir.mkdirs()) { - return true; - } - return false; + return dir.mkdirs(); } /** diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java new file mode 100644 index 000000000..55059418e --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java @@ -0,0 +1,164 @@ +/* + * 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.file; + +import static com.google.android.testing.mocking.AndroidMock.eq; +import static com.google.android.testing.mocking.AndroidMock.expect; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointsColumns; +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.location.LocationManager; +import android.net.Uri; +import android.test.AndroidTestCase; + +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.SimpleTimeZone; + +import org.easymock.Capture; + +/** + * Abstract class for testing importer. + * + * @author Jimmy Shih. + */ +public class AbstractTestImporter extends AndroidTestCase { + + protected static final String TRACK_NAME_0 = "blablub"; + protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer"; + + protected static final String TRACK_NAME_1 = "another track"; + protected static final String TRACK_DESCRIPTION_1 = "another description"; + + protected static final double TRACK_LATITUDE = 48.768364; + protected static final double TRACK_LONGITUDE = 9.177886; + protected static final double TRACK_ELEVATION = 324.0; + + protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z"; + protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123"; + protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123"; + protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123"; + + protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat( + "yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US); + protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); + + static { + /* + * We can't omit the timezones in the test, otherwise it'll use the local + * timezone and fail depending on where the test runner is. + */ + SimpleTimeZone utc = new SimpleTimeZone(0, "UTC"); + DATE_FORMAT_0.setTimeZone(utc); + DATE_FORMAT_1.setTimeZone(utc); + } + + protected static final long TRACK_ID_0 = 1; + protected static final long TRACK_ID_1 = 2; + protected static final long TRACK_POINT_ID_0 = 1; + protected static final long TRACK_POINT_ID_1 = 2; + protected static final long TRACK_POINT_ID_3 = 4; + protected static final long WAYPOINT_ID_0 = 1; + + protected static final Uri TRACK_ID_0_URI = ContentUris.appendId( + TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build(); + protected static final Uri TRACK_ID_1_URI = ContentUris.appendId( + TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build(); + protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId( + WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build(); + + protected MyTracksProviderUtils myTracksProviderUtils; + + private Factory oldMyTracksProviderUtilsFactory; + + @UsesMocks(MyTracksProviderUtils.class) + @Override + protected void setUp() throws Exception { + super.setUp(); + myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class); + oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance( + myTracksProviderUtils); + } + + @Override + protected void tearDown() throws Exception { + TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory); + super.tearDown(); + } + + protected Location createLocation(int index, long time) { + Location location = new Location(LocationManager.GPS_PROVIDER); + location.setLatitude(TRACK_LATITUDE + index); + location.setLongitude(TRACK_LONGITUDE + index); + location.setAltitude(TRACK_ELEVATION + index); + location.setTime(time); + return location; + } + + /** + * Expects the first track point to be added. + * + * @param location the location + * @param trackId the track id + * @param trackPointId the track point id + */ + protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) { + expect(myTracksProviderUtils.bulkInsertTrackPoint( + location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(), + eq(1), eq(trackId))).andReturn(1); + expect(myTracksProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId); + expect(myTracksProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId); + } + + /** + * Expects the track to be updated. + * + * @param track the track + * @param lastTrack true if it is the last track in the gpx + * @param trackId the track id + */ + protected void expectUpdateTrack(Capture track, boolean lastTrack, long trackId) { + myTracksProviderUtils.updateTrack(AndroidMock.capture(track)); + expect(myTracksProviderUtils.insertWaypoint((Waypoint) AndroidMock.anyObject())) + .andReturn(WAYPOINT_ID_O_URI); + if (lastTrack) { + // Return null to not add waypoints + expect(myTracksProviderUtils.getTrack(trackId)).andReturn(null); + } + } + + protected void verifyTrack(Track track, String name, String description, long time) { + assertEquals(name, track.getName()); + assertEquals(description, track.getDescription()); + if (time != -1L) { + assertEquals(time, track.getTripStatistics().getStartTime()); + } + assertNotSame(-1, track.getStartId()); + assertNotSame(-1, track.getStopId()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java index c9fda124f..48ed37630 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java @@ -13,78 +13,29 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.io.file; import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TracksColumns; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; -import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.testing.mocking.AndroidMock; -import com.google.android.testing.mocking.UsesMocks; -import android.content.ContentUris; import android.location.Location; -import android.location.LocationManager; -import android.net.Uri; -import android.test.AndroidTestCase; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Locale; -import java.util.SimpleTimeZone; - -import javax.xml.parsers.ParserConfigurationException; import org.easymock.Capture; -import org.easymock.IArgumentMatcher; import org.xml.sax.SAXException; /** - * Tests for the GPX importer. + * Tests for {@link GpxImporter}. * * @author Steffen Horlacher */ -public class GpxImporterTest extends AndroidTestCase { - - private static final String TRACK_NAME_0 = "blablub"; - private static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer"; - - private static final String TRACK_NAME_1 = "another track"; - private static final String TRACK_DESCRIPTION_1 = "another description"; - - private static final double TRACK_LATITUDE = 48.768364; - private static final double TRACK_LONGITUDE = 9.177886; - private static final double TRACK_ELEVATION = 324.0; - - private static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z"; - private static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123"; - private static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123"; - private static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123"; - - private static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat( - "yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US); - private static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); - - static { - /* - * We can't omit the timezones in the test, otherwise it'll use the local - * timezone and fail depending on where the test runner is. - */ - SimpleTimeZone utc = new SimpleTimeZone(0, "UTC"); - DATE_FORMAT_0.setTimeZone(utc); - DATE_FORMAT_1.setTimeZone(utc); - } +public class GpxImporterTest extends AbstractTestImporter { private static String getNameAndDescription(String name, String description) { return "" + ""; @@ -134,40 +85,6 @@ public class GpxImporterTest extends AndroidTestCase { private static final String INVALID_LONGITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll( Double.toString(TRACK_LONGITUDE), "invalid"); - private static final long TRACK_ID_0 = 1; - private static final long TRACK_ID_1 = 2; - private static final long TRACK_POINT_ID_0 = 1; - private static final long TRACK_POINT_ID_1 = 2; - private static final long TRACK_POINT_ID_3 = 4; - private static final long WAYPOINT_ID_0 = 1; - - private static final Uri TRACK_ID_0_URI = ContentUris.appendId( - TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build(); - private static final Uri TRACK_ID_1_URI = ContentUris.appendId( - TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build(); - private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId( - WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build(); - - private MyTracksProviderUtils myTracksProviderUtils; - - private Factory oldMyTracksProviderUtilsFactory; - - @UsesMocks(MyTracksProviderUtils.class) - @Override - protected void setUp() throws Exception { - super.setUp(); - - myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class); - oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance( - myTracksProviderUtils); - } - - @Override - protected void tearDown() throws Exception { - TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory); - super.tearDown(); - } - /** * Tests one track with one segment. */ @@ -190,8 +107,8 @@ public class GpxImporterTest extends AndroidTestCase { AndroidMock.replay(myTracksProviderUtils); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); - long[] trackIds = GpxImporter.importGPXFile(getContext(), inputStream, myTracksProviderUtils, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + GpxImporter gpxImporter = new GpxImporter(getContext(), myTracksProviderUtils); + long[] trackIds = gpxImporter.importFile(inputStream); assertEquals(1, trackIds.length); assertEquals(TRACK_ID_0, trackIds[0]); @@ -223,8 +140,8 @@ public class GpxImporterTest extends AndroidTestCase { AndroidMock.replay(myTracksProviderUtils); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); - long[] trackIds = GpxImporter.importGPXFile(getContext(), inputStream, myTracksProviderUtils, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + GpxImporter gpxImporter = new GpxImporter(getContext(), myTracksProviderUtils); + long[] trackIds = gpxImporter.importFile(inputStream); assertEquals(1, trackIds.length); assertEquals(TRACK_ID_0, trackIds[0]); @@ -260,8 +177,9 @@ public class GpxImporterTest extends AndroidTestCase { InputStream inputStream = new ByteArrayInputStream( VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes()); - long[] trackIds = GpxImporter.importGPXFile(getContext(), inputStream, myTracksProviderUtils, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + GpxImporter gpxImporter = new GpxImporter(getContext(), myTracksProviderUtils); + long[] trackIds = gpxImporter.importFile(inputStream); + assertEquals(1, trackIds.length); assertEquals(TRACK_ID_0, trackIds[0]); @@ -294,8 +212,8 @@ public class GpxImporterTest extends AndroidTestCase { AndroidMock.replay(myTracksProviderUtils); InputStream inputStream = new ByteArrayInputStream(VALID_TWO_TRACKS_GPX.getBytes()); - long[] trackIds = GpxImporter.importGPXFile(getContext(), inputStream, myTracksProviderUtils, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + GpxImporter gpxImporter = new GpxImporter(getContext(), myTracksProviderUtils); + long[] trackIds = gpxImporter.importFile(inputStream); assertEquals(2, trackIds.length); assertEquals(TRACK_ID_0, trackIds[0]); assertEquals(TRACK_ID_1, trackIds[1]); @@ -306,46 +224,46 @@ public class GpxImporterTest extends AndroidTestCase { /** * Test an invalid xml input. */ - public void testInvalidXml() throws ParserConfigurationException, IOException { + public void testInvalidXml() throws Exception { testInvalidGpx(INVALID_XML_GPX); } /** * Test an invalid location. */ - public void testInvalidLocation() throws ParserConfigurationException, IOException { + public void testInvalidLocation() throws Exception { testInvalidGpx(INVALID_LOCATION_GPX); } /** * Test an invalid time. */ - public void testInvalidTime() throws ParserConfigurationException, IOException { + public void testInvalidTime() throws Exception { testInvalidGpx(INVALID_TIME_GPX); } /** * Test an invalid altitude. */ - public void testInvalidAltitude() throws ParserConfigurationException, IOException { + public void testInvalidAltitude() throws Exception { testInvalidGpx(INVALID_ALTITUDE_GPX); } /** * Test an invalid latitude. */ - public void testInvalidLatitude() throws ParserConfigurationException, IOException { + public void testInvalidLatitude() throws Exception { testInvalidGpx(INVALID_LATITUDE_GPX); } /** * Test an invalid longitude. */ - public void testInvalidLongitude() throws ParserConfigurationException, IOException { + public void testInvalidLongitude() throws Exception { testInvalidGpx(INVALID_LONGITUDE_GPX); } - private void testInvalidGpx(String xml) throws ParserConfigurationException, IOException { + private void testInvalidGpx(String xml) throws Exception { expect(myTracksProviderUtils.insertTrack((Track) AndroidMock.anyObject())) .andReturn(TRACK_ID_0_URI); @@ -361,116 +279,11 @@ public class GpxImporterTest extends AndroidTestCase { InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); try { - GpxImporter.importGPXFile(getContext(), inputStream, myTracksProviderUtils, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + GpxImporter gpxImporter = new GpxImporter(getContext(), myTracksProviderUtils); + gpxImporter.importFile(inputStream); } catch (SAXException e) { // expected } AndroidMock.verify(myTracksProviderUtils); } - - private Location createLocation(int index, long time) { - Location location = new Location(LocationManager.GPS_PROVIDER); - location.setLatitude(TRACK_LATITUDE + index); - location.setLongitude(TRACK_LONGITUDE + index); - location.setAltitude(TRACK_ELEVATION + index); - location.setTime(time); - return location; - } - - /** - * Expects the first track point to be added. - * - * @param location the location - * @param trackId the track id - * @param trackPointId the track point id - */ - private void expectFirstTrackPoint(Location location, long trackId, long trackPointId) { - expect(myTracksProviderUtils.bulkInsertTrackPoint( - location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(), - eq(1), eq(trackId))).andReturn(1); - expect(myTracksProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId); - expect(myTracksProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId); - } - - /** - * Expects the track to be updated. - * - * @param track the track - * @param lastTrack true if it is the last track in the gpx - * @param trackId the track id - */ - private void expectUpdateTrack(Capture track, boolean lastTrack, long trackId) { - myTracksProviderUtils.updateTrack(AndroidMock.capture(track)); - expect(myTracksProviderUtils.insertWaypoint((Waypoint) AndroidMock.anyObject())) - .andReturn(WAYPOINT_ID_O_URI); - if (lastTrack) { - // Return null to not add waypoints - expect(myTracksProviderUtils.getTrack(trackId)).andReturn(null); - } - } - - private void verifyTrack(Track track, String name, String description, long time) { - assertEquals(name, track.getName()); - assertEquals(description, track.getDescription()); - if (time != -1L) { - assertEquals(time, track.getTripStatistics().getStartTime()); - } - assertNotSame(-1, track.getStartId()); - assertNotSame(-1, track.getStopId()); - } - - /** - * Workaround because of capture bug 2617107 in easymock: - * http://sourceforge.net - * /tracker/?func=detail&aid=2617107&group_id=82958&atid=567837 - */ - private static class LocationsMatcher implements IArgumentMatcher { - private final Location[] matchLocs; - - private LocationsMatcher(Location[] expected) { - this.matchLocs = expected; - } - - public static Location[] eqLoc(Location[] expected) { - IArgumentMatcher matcher = new LocationsMatcher(expected); - AndroidMock.reportMatcher(matcher); - return null; - } - - public static Location[] eqLoc(Location expected) { - return eqLoc(new Location[] { expected }); - } - - @Override - public void appendTo(StringBuffer buf) { - buf.append("eqLoc(").append(Arrays.toString(matchLocs)).append(")"); - } - - @Override - public boolean matches(Object obj) { - if (!(obj instanceof Location[])) { - return false; - } - Location[] locs = (Location[]) obj; - if (locs.length < matchLocs.length) { - return false; - } - - // Only check the first elements (those that will be taken into account) - for (int i = 0; i < matchLocs.length; i++) { - if (!locationsMatch(locs[i], matchLocs[i])) { - return false; - } - } - - return true; - } - - private boolean locationsMatch(Location loc1, Location loc2) { - return (loc1.getTime() == loc2.getTime()) && (loc1.getLatitude() == loc2.getLatitude()) - && (loc1.getLongitude() == loc2.getLongitude()) - && (loc1.getAltitude() == loc2.getAltitude()); - } - } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlImporterTest.java new file mode 100644 index 000000000..3184ce575 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlImporterTest.java @@ -0,0 +1,137 @@ +/* + * 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.file; + +import static com.google.android.testing.mocking.AndroidMock.eq; +import static com.google.android.testing.mocking.AndroidMock.expect; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.testing.mocking.AndroidMock; + +import android.location.Location; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import org.easymock.Capture; + +/** + * Tests for {@link KmlImporter}. + * + * @author Jimmy Shih + */ +public class KmlImporterTest extends AbstractTestImporter { + + private static String getNameAndDescription(String name, String description) { + return ""; + } + + private static String getTrackPoint(int index, String time) { + String latitude = Double.toString(TRACK_LATITUDE + index); + String longitude = Double.toString(TRACK_LONGITUDE + index); + String altitude = Double.toString(TRACK_ELEVATION + index); + StringBuffer buffer = new StringBuffer(); + buffer.append("" + time + "" + "" + longitude + " " + latitude + " " + + altitude + ""); + return buffer.toString(); + } + + private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = + "" + + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "" + + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + + ""; + private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = + "" + + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "" + + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "" + + getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3) + + ""; + + /** + * Tests one track with one segment. + */ + public void testOneTrackOneSegment() throws Exception { + Capture track = new Capture(); + + Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); + Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); + + expect(myTracksProviderUtils.insertTrack((Track) AndroidMock.anyObject())) + .andReturn(TRACK_ID_0_URI); + expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); + + // A flush happens at the end + expect(myTracksProviderUtils.bulkInsertTrackPoint( + LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1); + expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1); + + expectUpdateTrack(track, true, TRACK_ID_0); + AndroidMock.replay(myTracksProviderUtils); + + InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); + KmlImporter kmlImporter = new KmlImporter(getContext(), myTracksProviderUtils); + long[] trackIds = kmlImporter.importFile(inputStream); + assertEquals(1, trackIds.length); + assertEquals(TRACK_ID_0, trackIds[0]); + + long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); + long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); + assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime()); + AndroidMock.verify(myTracksProviderUtils); + verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); + } + + /** + * Tests one track with two segments. + */ + public void testOneTrackTwoSegments() throws Exception { + Capture track = new Capture(); + + Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); + + expect(myTracksProviderUtils.insertTrack((Track) AndroidMock.anyObject())) + .andReturn(TRACK_ID_0_URI); + expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); + + // A flush happens at the end + expect(myTracksProviderUtils.bulkInsertTrackPoint( + (Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5); + expect(myTracksProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3); + + expectUpdateTrack(track, true, TRACK_ID_0); + AndroidMock.replay(myTracksProviderUtils); + + InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); + KmlImporter kmlImporter = new KmlImporter(getContext(), myTracksProviderUtils); + long[] trackIds = kmlImporter.importFile(inputStream); + assertEquals(1, trackIds.length); + assertEquals(TRACK_ID_0, trackIds[0]); + + long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); + long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); + long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime(); + long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); + assertEquals( + time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime()); + + AndroidMock.verify(myTracksProviderUtils); + verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, + DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/LocationsMatcher.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/LocationsMatcher.java new file mode 100644 index 000000000..7fed88299 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/LocationsMatcher.java @@ -0,0 +1,81 @@ +/* + * 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.file; + +import com.google.android.testing.mocking.AndroidMock; + +import android.location.Location; + +import java.util.Arrays; + +import org.easymock.IArgumentMatcher; + +/** + * Locations matcher. Workaround because of capture bug 2617107 in easymock: + * http://sourceforge.net/tracker/?func=detail&aid=2617107&group_id=82958&atid=567837 + * + * @author Jimmy Shih + */ +public class LocationsMatcher implements IArgumentMatcher { + + public static Location[] eqLoc(Location expected) { + return eqLoc(new Location[] { expected }); + } + + public static Location[] eqLoc(Location[] expected) { + IArgumentMatcher matcher = new LocationsMatcher(expected); + AndroidMock.reportMatcher(matcher); + return null; + } + + private final Location[] expectedLocations; + + private LocationsMatcher(Location[] expected) { + this.expectedLocations = expected; + } + + @Override + public void appendTo(StringBuffer buf) { + buf.append("eqLoc(").append(Arrays.toString(expectedLocations)).append(")"); + } + + @Override + public boolean matches(Object obj) { + if (!(obj instanceof Location[])) { + return false; + } + Location[] locations = (Location[]) obj; + if (locations.length < expectedLocations.length) { + return false; + } + + // Only check the first elements (those that will be taken into account) + for (int i = 0; i < expectedLocations.length; i++) { + if (!matchLocation(locations[i], expectedLocations[i])) { + return false; + } + } + return true; + } + + private boolean matchLocation(Location location1, Location location2) { + return (location1.getTime() == location2.getTime()) + && (location1.getLatitude() == location2.getLatitude()) + && (location1.getLongitude() == location2.getLongitude()) + && (location1.getAltitude() == location2.getAltitude()); + } +} diff --git a/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/ExportAndImportTest.java b/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/ExportAndImportTest.java index 33d71eae8..6a3d1b1f3 100644 --- a/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/ExportAndImportTest.java +++ b/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/ExportAndImportTest.java @@ -106,7 +106,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2