Add KmlImporter.

This commit is contained in:
Jimmy Shih
2013-01-07 15:54:04 -08:00
parent c14f2af060
commit 2e69c47e2a
24 changed files with 1613 additions and 1075 deletions
+2 -1
View File
@@ -45,7 +45,8 @@ limitations under the License.
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<!-- Permission to read phone state so voice announcement will not interrupt ongoing calls -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- Permission to write to SD card -->
<!-- Permission to read/write external storage -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Permissions to access My Tracks data -->
<uses-permission android:name="com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS" />
+23 -6
View File
@@ -28,17 +28,34 @@ limitations under the License.
android:title="@string/menu_start_gps"/>
<item
android:icon="@drawable/menu_import"
android:id="@+id/track_list_import"
android:title="@string/menu_import"/>
android:id="@+id/track_list_import_all"
android:title="@string/menu_import_all">
<menu>
<item
android:id="@+id/track_list_import_all_gpx"
android:title=""/>
<item
android:id="@+id/track_list_import_all_kml"
android:title=""/>
</menu>
</item>
<item
android:icon="@drawable/menu_save"
android:id="@+id/track_list_save_all"
android:title="@string/menu_save_all">
<menu>
<item android:id="@+id/track_list_save_all_gpx"/>
<item android:id="@+id/track_list_save_all_kml"/>
<item android:id="@+id/track_list_save_all_csv"/>
<item android:id="@+id/track_list_save_all_tcx"/>
<item
android:id="@+id/track_list_save_all_gpx"
android:title=""/>
<item
android:id="@+id/track_list_save_all_kml"
android:title=""/>
<item
android:id="@+id/track_list_save_all_csv"
android:title=""/>
<item
android:id="@+id/track_list_save_all_tcx"
android:title=""/>
</menu>
</item>
<item
+7 -7
View File
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<plurals name="importGpxFiles">
<item quantity="zero">@string/import_gpx_files_zero</item>
<item quantity="one">@string/import_gpx_files_one</item>
<item quantity="two">@string/import_gpx_files_two</item>
<item quantity="few">@string/import_gpx_files_few</item>
<item quantity="many">@string/import_gpx_files_many</item>
<item quantity="other">@string/import_gpx_files_other</item>
<plurals name="importFiles">
<item quantity="zero">@string/import_files_zero</item>
<item quantity="one">@string/import_files_one</item>
<item quantity="two">@string/import_files_two</item>
<item quantity="few">@string/import_files_few</item>
<item quantity="many">@string/import_files_many</item>
<item quantity="other">@string/import_files_other</item>
</plurals>
<plurals name="voiceHours">
<item quantity="zero">@string/voice_hours_zero</item>
+86 -55
View File
@@ -604,39 +604,13 @@ limitations under the License.
that the operation was canceled.">
The operation was canceled.
</string>
<string name="external_storage_error_no_storage"
translation_description="When importing/saving to the external storage, the error message
indicating that no external storage is found.">
No external storage is found.
<string name="external_storage_not_available"
translation_description="Error message when the external storage is not available.">
External storage not available.
</string>
<string name="external_storage_import_error"
translation_description="When importing from the external storage, the error message indicating
not able to import.">
Unable to import from external storage.
</string>
<string name="external_storage_import_error_no_file"
translation_description="When importing from the external storage, the error message indicating
that no file is found on an external storage directory.
%1$s: the external storage directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
No file found in %1$s.
</string>
<string name="external_storage_import_progress_message"
translation_description="When importing from the external storage, the progress message.">
Importing from external storage&#8230;
</string>
<string name="external_storage_import_success"
translation_description="When importing from the external storage, the success message.">
Finished importing from external storage.
</string>
<string name="external_storage_import_success_count"
translation_description="When importing from the external storage, the success message with a
count of the imported files.
%1$d the number of successfully imported tracks. E.g., '1'.
%2$s the number of files in the external storage directory. E.g., '2 GPX files'.
%3$s the external storage directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Imported %1$d of %2$s from %3$s.
<string name="external_storage_not_writeable"
translation_description="Error messazge when the external storage is not writeable.">
External storage not writeable.
</string>
<string name="external_storage_save_error"
translation_description="When saving to the external storage, the error message indicating not
@@ -986,37 +960,76 @@ limitations under the License.
Track
</string>
<!-- Import GPX Files -->
<string name="import_gpx_files_few"
translation_description="When importing all the tracks, phrase for 'few' GPX files.
<!-- Import -->
<string name="import_files_few"
translation_description="When importing files, the phrase for 'few' files. This is used when the
language requires special treatment of 'small' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a small number.">
%1$d GPX files
%1$d files
</string>
<string name="import_gpx_files_many"
translation_description="When importing all the tracks, phrase for 'many' GPX files.
<string name="import_files_many"
translation_description="When importing files, the phrase for 'many' files. This is used when
the language requires special treatment of 'large' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a large number.">
%1$d GPX files
%1$d files
</string>
<string name="import_gpx_files_one"
translation_description="When importing all the tracks, phrase for 1 GPX file.">
1 GPX file
<string name="import_files_one"
translation_description="When importing files, the phrase for 1 file. This is used when the
language requires special treatment of numbers like one. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
1 file
</string>
<string name="import_gpx_files_other"
translation_description="When importing all the tracks, phrase for 'other' GPX files, 'other'
means for cases that the language does not require special treatment of the given quantity.
<string name="import_files_other"
translation_description="When importing files, the phrase for 'other' files. This is used when
the language does not require special treatment of the given quantity (as with all numbers in
Chinese). See http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number.">
%1$d GPX files
%1$d files
</string>
<string name="import_gpx_files_two"
translation_description="When importing all the tracks, phrase for 2 GPX files.">
2 GPX files
<string name="import_files_two"
translation_description="When importing files, the phrase for 2 files. This is used when the
language requires special treatment of numbers like two. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
2 files
</string>
<string name="import_gpx_files_zero"
translation_description="When importing all the tracks, phrase for 0 GPX files.">
0 GPX files
<string name="import_files_zero"
translation_description="When importing files, the phrase for 0 files. This is used when the
language requires special treatment of the number 0. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
0 files
</string>
<string name="import_no_directory"
translation_description="When importing from the external storage, the error message when the
directory doesn't exist.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
%1$s does not exist.
</string>
<string name="import_no_file"
translation_description="When importing from the external storage, the message indicating
that no file is found.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
No file found in %1$s.
</string>
<string name="import_progress_message"
translation_description="When importing from the external storage, the progress message.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Importing from %1$s&#8230;
</string>
<string name="import_success"
translation_description="When importing from the external storage, the success message.
%1$d the number of files successfully imported. E.g., '2'.
%2$s the number of files in the directory. E.g., '2 files'.
%3$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Imported %1$d of %2$s from %3$s.
</string>
<!-- Maps List -->
@@ -1149,11 +1162,19 @@ limitations under the License.
Character limit: ~20 (1 line)">
Help
</string>
<string name="menu_import"
translation_description="In the menu, to import tracks from the external storage.
<string name="menu_import_all"
translation_description="In the menu, to import all tracks from the external storage.
Character limit: ~20 (1 line)">
Import from external storage
Import all from external storage
</string>
<string name="menu_import_all_format"
translation_description="Under the import all menu option, the submenu option to import all
files of a certain file format.
%1$s: file format. e.g., 'GPX', 'KML'
Character limit: ~20 (1 line)">
Import %1$s
</string>
<string name="menu_insert_marker"
translation_description="In the menu, to insert a marker.
@@ -1352,6 +1373,16 @@ limitations under the License.
write data to Google My Tracks and start/stop Google My Tracks recording
</string>
<!-- Restore -->
<string name="restore_error"
translation_description="When restoring from backup, the error message.">
Unable to restore from backup.
</string>
<string name="restore_success"
translation_description="When restoring from backup, the success message.">
Finished restoring from backup.
</string>
<!-- Search -->
<string name="search_global_description"
translation_description="Description of what is searched in My Tracks, to be displayed in the
@@ -60,6 +60,7 @@ import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import java.util.List;
import java.util.Locale;
/**
* An activity to show the track detail.
@@ -506,7 +507,8 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save");
AnalyticsUtils.sendPageViews(
this, "/action/save_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_ID, trackId)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
@@ -81,6 +81,7 @@ import android.widget.ListView;
import android.widget.Toast;
import java.util.EnumSet;
import java.util.Locale;
/**
* An activity displaying a list of tracks.
@@ -323,7 +324,7 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem importMenuItem;
private MenuItem importAllMenuItem;
private MenuItem saveAllMenuItem;
private MenuItem deleteAllMenuItem;
@@ -486,6 +487,12 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_list, menu);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
// Import all submenu titles
menu.findItem(R.id.track_list_import_all_gpx)
.setTitle(getString(R.string.menu_import_all_format, fileTypes[0]));
menu.findItem(R.id.track_list_import_all_kml)
.setTitle(getString(R.string.menu_import_all_format, fileTypes[1]));
// Save all submenu titles
menu.findItem(R.id.track_list_save_all_gpx)
.setTitle(getString(R.string.menu_save_format, fileTypes[0]));
menu.findItem(R.id.track_list_save_all_kml)
@@ -497,7 +504,7 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
searchMenuItem = menu.findItem(R.id.track_list_search);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
importMenuItem = menu.findItem(R.id.track_list_import);
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
saveAllMenuItem = menu.findItem(R.id.track_list_save_all);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
@@ -529,11 +536,11 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
handleStartGps();
updateMenuItems(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
return true;
case R.id.track_list_import:
AnalyticsUtils.sendPageViews(this, "/action/import");
intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true);
startActivity(intent);
case R.id.track_list_import_all_gpx:
startImportActivity(TrackFileFormat.GPX);
return true;
case R.id.track_list_import_all_kml:
startImportActivity(TrackFileFormat.KML);
return true;
case R.id.track_list_save_all_gpx:
startSaveActivity(TrackFileFormat.GPX);
@@ -653,8 +660,8 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
startGpsMenuItem.setTitle(startGps ? R.string.menu_stop_gps : R.string.menu_start_gps);
startGpsMenuItem.setVisible(!isRecording);
}
if (importMenuItem != null) {
importMenuItem.setVisible(!isRecording);
if (importAllMenuItem != null) {
importAllMenuItem.setVisible(!isRecording);
}
if (saveAllMenuItem != null) {
saveAllMenuItem.setVisible(!isRecording);
@@ -680,13 +687,28 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
bindChangedCallback.run();
}
/**
* Starts the {@link ImportActivity} to import all tracks.
*
* @param trackFileFormat the track file format
*/
private void startImportActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(
this, "/action/import_all_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true)
.putExtra(ImportActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
}
/**
* Starts the {@link SaveActivity} to save all tracks.
*
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save_all");
AnalyticsUtils.sendPageViews(
this, "/action/save_all_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
@@ -80,8 +80,8 @@ public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@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;
}
@@ -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;
}
}
@@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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);
@@ -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;
}
@@ -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<Long> trackIds;
private final List<Waypoint> 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<Long>();
waypoints = new ArrayList<Waypoint>();
}
@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;
}
}
@@ -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<Long> 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<Waypoint> waypoints = new ArrayList<Waypoint>();
/*
* 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<Long>();
@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);
}
}
@@ -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
*/
@@ -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<Void, Integer, Boolean> {
@@ -48,20 +40,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
/**
* 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<Void, Integer, Boolean> {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
success = false;
completed = false;
successCount = 0;
totalCount = 0;
@@ -98,13 +87,13 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* 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<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
if (!FileUtils.isSdCardAvailable()) {
return false;
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
@@ -133,8 +118,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
// 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<Void, Integer, Boolean> {
@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<Void, Integer, Boolean> {
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);
}
}
@@ -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<Location> locationList;
private ArrayList<Integer> cadenceList;
private ArrayList<Integer> heartRateList;
private ArrayList<Integer> 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<Location>();
powerList = new ArrayList<Integer>();
cadenceList = new ArrayList<Integer>();
heartRateList = new ArrayList<Integer>();
}
/**
* 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);
}
}
}
}
@@ -303,6 +303,8 @@ public class KmlTrackWriter implements TrackFormatWriter {
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
@@ -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;
}
@@ -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();
}
/**
@@ -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> 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());
}
}
@@ -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 "<name><![CDATA[" + name + "]]></name>" + "<desc><![CDATA[" + description + "]]></desc>";
@@ -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> 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());
}
}
}
@@ -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 "<name><![CDATA[" + name + "]]></name><description><![CDATA[" + description
+ "]]></description>";
}
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("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " "
+ altitude + "</gx:coord>");
return buffer.toString();
}
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX =
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX =
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
/**
* Tests one track with one segment.
*/
public void testOneTrackOneSegment() throws Exception {
Capture<Track> track = new Capture<Track>();
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> track = new Capture<Track>();
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());
}
}
@@ -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());
}
}
@@ -106,7 +106,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
// No file to imported.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_import), true);
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(
R.string.external_storage_import_error_no_file,
R.string.import_no_file,
FileUtils.buildExternalDirectoryPath(EndToEndTestUtils.GPX)));
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
@@ -133,7 +133,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
// Wait for the prefix of import success string is much faster than wait
// the whole string.
EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.external_storage_import_success_count).split("%")[0]);
activityMyTracks.getString(R.string.import_success).split("%")[0]);
// Check import tracks should be equal with the sum of trackNumber and
// gpxFilesNumber;
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
@@ -264,7 +264,7 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.external_storage_import_success), 0,
activityMyTracks.getString(R.string.restore_success), 0,
EndToEndTestUtils.SUPER_LONG_WAIT_TIME));
// Check restore track.
assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.trackName));