/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import com.google.android.apps.mymaps.MyMapsFeature;
import com.google.android.apps.mymaps.MyMapsGDataConverter;
import com.google.android.apps.mymaps.MyMapsGDataWrapper;
import com.google.android.apps.mymaps.MyMapsGDataWrapper.QueryFunction;
import com.google.android.apps.mymaps.MyMapsMapMetadata;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.ProgressIndicator;
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.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.maps.MapsClient;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* A helper class used to transmit tracks to Google MyMaps.
* A new instance should be used for each upload.
*
* @author Leif Hendrik Wilden
*/
public class SendToMyMaps implements Runnable {
public static final String NEW_MAP_ID = "new";
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
private String mapId;
// GData service
private MyMapsGDataWrapper wrapper;
private MyMapsGDataConverter gdataConverter;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
public interface OnSendCompletedListener {
void onSendCompleted(String mapId, boolean success, int statusMessage);
}
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void run() {
Log.d(MyTracksConstants.TAG, "Sending to MyMaps: trackId = " + trackId);
doUpload();
}
private void doUpload() {
int statusMessageId = R.string.error_sending_to_mymap;
boolean success = true;
try {
gdataConverter = new MyMapsGDataConverter();
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
String originalDescription = track.getDescription();
track.setDescription("
" + track.getDescription() + "
"
+ stringUtils.generateTrackDescription(track, null, null) + "
");
wrapper = new MyMapsGDataWrapper(context);
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
if (isNewMap) {
success = createNewMap(track, originalDescription);
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Put waypoints.
if (success) {
success = uploadWaypoints(track);
if (!success) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: upload waypoints failed.");
}
}
if (success) {
statusMessageId = isNewMap
? R.string.status_new_mymap_has_been_created
: R.string.status_tracks_have_been_uploaded;
}
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Done: " + success);
progressIndicator.setProgressValue(100);
} catch (XmlPullParserException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
} finally {
if (wrapper != null) {
wrapper.cleanUp();
}
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
mapId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
/**
* Creates a new map for the given track.
*
* @param track The track that will be uploaded to this map
* @return True on success.
*/
private boolean createNewMap(final Track track, final String description) {
progressIndicator.setProgressMessage(
R.string.progress_message_creating_map);
return wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
Log.d(MyTracksConstants.TAG, "Creating a new map.");
String mapFeed = MapsClient.getMapsFeed();
Log.d(MyTracksConstants.TAG, "Map feed is " + mapFeed);
MyMapsMapMetadata metaData = new MyMapsMapMetadata();
metaData.setTitle(track.getName());
metaData.setDescription(description + " - "
+ track.getCategory() + " - "
+ context.getString(R.string.new_map_description));
SharedPreferences preferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
boolean mapPublic = true;
if (preferences != null) {
mapPublic = preferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
}
metaData.setSearchable(mapPublic);
Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
Log.d(MyTracksConstants.TAG, "Title: " + entry.getTitle());
Entry map = client.createEntry(mapFeed, auth.getAuthToken(), entry);
mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
Log.d(MyTracksConstants.TAG, "New map id is: " + mapId);
}
});
}
private boolean uploadAllTrackPoints(
final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
}
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (!locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(MyTracksConstants.TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector distances = new Vector();
Vector elevations = new Vector();
DoubleBuffer elevationBuffer =
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
List locations = new ArrayList(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
uploadMarker(track, loc, true);
}
// Add to the elevation profile.
if (loc != null && MyTracksUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
== MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("" + originalDescription + "
"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "
");
return uploadMarker(track, lastLocation, false);
}
return true;
} finally {
locationsCursor.close();
}
}
private boolean uploadMarker(final Track track,
final Location location,
final boolean isStart) {
boolean okay = wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client)
throws AuthenticationException, IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
insertMarker(context, client, featureFeed, track, location, isStart);
}
});
return okay;
}
/**
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
* 2nd failure.
*/
private void insertMarker(Context context, MapsClient client,
String featureFeed, Track track, Location loc,
boolean isStart) throws IOException, Exception {
Entry entry = gdataConverter.getEntryForFeature(
buildMyMapsPlacemarkFeature(context, track, loc, isStart));
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Creating placemark "
+ entry.getTitle());
try {
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG, "SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
}
/**
* Builds a placemark MyMapsFeature from a track.
*
* @param track the track
* @param isStart true if it's the start of the track, or false for end
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsPlacemarkFeature(Context context,
Track track, Location loc, boolean isStart) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.MARKER);
if (isStart) {
myMapsFeature.setIconUrl(START_ICON_URL);
} else {
myMapsFeature.setIconUrl(END_ICON_URL);
}
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
String name = track.getName() + " "
+ (isStart ? context.getString(R.string.start)
: context.getString(R.string.end));
myMapsFeature.setTitle(name);
myMapsFeature.setDescription(isStart ? "" : track.getDescription());
return myMapsFeature;
}
private boolean prepareAndUploadPoints(Track track, List locations) {
progressIndicator.setProgressMessage(
R.string.progress_message_preparing_track);
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList