diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 64985ca42..69b07f8ec 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -248,6 +248,7 @@ limitations under the License. + - (End) + translation_description="When inserting a marker to indicate the end of a track, the marker's + name. E.g., 'track_name (End)'. + + %1$s track name. E.g., 'track_name'."> + %1$s (End) - (Start) + translation_description="When inserting a marker to indicate the start of a track, the marker's + name. E.g., 'track_name (Start)'. + + %1$s track name. E.g., 'track_name'."> + %1$s (Start) Preparing track... + + Sending to %1$s. Please wait... + @@ -1226,11 +1237,12 @@ limitations under the License. Send to... - (Part %1$d) + %1$s: track name. E.g., 'track_name' + %2$d: part number. E.g., '1'."> + %1$s (Part %2$d) diff --git a/MyTracks/src/com/google/android/apps/mytracks/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index a6d2a3df8..6af0b202b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -44,20 +44,20 @@ public abstract class Constants { public static final int GET_MAP = 1; public static final int SHOW_TRACK = 2; public static final int AUTHENTICATE_TO_MAPS = 3; - public static final int AUTHENTICATE_TO_FUSION_TABLES = 4; - public static final int AUTHENTICATE_TO_DOCLIST = 5; - public static final int AUTHENTICATE_TO_TRIX = 6; - public static final int SHARE_GPX_FILE = 7; - public static final int SHARE_KML_FILE = 8; - public static final int SHARE_CSV_FILE = 9; - public static final int SHARE_TCX_FILE = 10; - public static final int SAVE_GPX_FILE = 11; - public static final int SAVE_KML_FILE = 12; - public static final int SAVE_CSV_FILE = 13; - public static final int SAVE_TCX_FILE = 14; - public static final int SHOW_WAYPOINT = 15; - public static final int WELCOME = 16; - public static final int CHOOSE_ACCOUNT = 17; + public static final int AUTHENTICATE_TO_DOCLIST = 4; + public static final int AUTHENTICATE_TO_TRIX = 5; + public static final int SHARE_GPX_FILE = 6; + public static final int SHARE_KML_FILE = 7; + public static final int SHARE_CSV_FILE = 8; + public static final int SHARE_TCX_FILE = 9; + public static final int SAVE_GPX_FILE = 10; + public static final int SAVE_KML_FILE = 11; + public static final int SAVE_CSV_FILE = 12; + public static final int SAVE_TCX_FILE = 13; + public static final int SHOW_WAYPOINT = 14; + public static final int WELCOME = 15; + public static final int CHOOSE_ACCOUNT = 16; + public static final int SEND_FUSION_TABLES = 17; /* * Menu ids: diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java deleted file mode 100644 index 48e55938b..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ /dev/null @@ -1,716 +0,0 @@ -/* - * Copyright 2010 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 static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.Constants; -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.io.gdata.GDataWrapper; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction; -import com.google.android.apps.mytracks.io.sendtogoogle.SendType; -import com.google.android.apps.mytracks.stats.DoubleBuffer; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.SystemUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; -import com.google.api.client.googleapis.GoogleHeaders; -import com.google.api.client.googleapis.MethodOverride; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.InputStreamContent; -import com.google.api.client.util.Strings; - -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.ByteArrayInputStream; -import java.io.IOException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Vector; - -/** - * A helper class used to transmit tracks to Google Fusion Tables. - * A new instance should be used for each upload. - * - * @author Leif Hendrik Wilden - */ -public class SendToFusionTables implements Runnable { - - private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; - - /** - * Listener invoked when sending to fusion tables completes. - */ - public interface OnSendCompletedListener { - void onSendCompleted(String tableId, boolean success); - } - - /** The GData service id for Fusion Tables. */ - public static final String SERVICE_ID = "fusiontables"; - - /** The path for viewing a map visualization of a table. */ - private static final String FUSIONTABLES_MAP = - "https://www.google.com/fusiontables/embedviz?" + - "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&" + - "lat=%f&lng=%f&z=%d&t=1&l=col2"; - - /** Standard base feed url for Fusion Tables. */ - private static final String FUSIONTABLES_BASE_FEED_URL = - "https://www.google.com/fusiontables/api/query"; - - private static final int MAX_POINTS_PER_UPLOAD = 2048; - - private static final String GDATA_VERSION = "2"; - - // This class reports upload status to the user as a completion percentage - // using a progress bar. Progress is defined as follows: - // - // 0% Getting track metadata - // 5% Creating Fusion Table (GData to FT server) - // 10%-90% Uploading the track data to Fusion Tables - // 95% Uploading waypoints - // 100% Done - private static final int PROGRESS_INITIALIZATION = 0; - private static final int PROGRESS_FUSION_TABLE_CREATE = 5; - private static final int PROGRESS_UPLOAD_DATA_MIN = 10; - private static final int PROGRESS_UPLOAD_DATA_MAX = 90; - private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; - private static final int PROGRESS_COMPLETE = 100; - - 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; - - // Progress status - private int totalLocationsRead; - private int totalLocationsPrepared; - private int totalLocationsUploaded; - private int totalLocations; - private int totalSegmentsUploaded; - - private HttpRequestFactory httpRequestFactory; - private String tableId; - - private static String MARKER_TYPE_START = "large_green"; - private static String MARKER_TYPE_END = "large_red"; - private static String MARKER_TYPE_WAYPOINT = "large_yellow"; - - public SendToFusionTables(Activity context, AuthManager auth, long trackId, - ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) { - this.context = context; - this.auth = auth; - this.trackId = trackId; - this.progressIndicator = progressIndicator; - this.onCompletion = onCompletion; - this.stringUtils = new StringUtils(context); - this.providerUtils = MyTracksProviderUtils.Factory.get(context); - - HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport(); - httpRequestFactory = transport.createRequestFactory(new MethodOverride()); - } - - @Override - public void run() { - Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId); - doUpload(); - } - - public static String getMapVisualizationUrl(Track track) { - if (track == null || track.getStatistics() == null || track.getTableId() == null) { - Log.w(TAG, "Unable to get track URL"); - return null; - } - - // TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track. - TripStatistics stats = track.getStatistics(); - double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2; - double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2; - int z = 15; - - // We explicitly format with Locale.US because we need the latitude and - // longitude to be formatted in a locale-independent manner. Specifically, - // we need the decimal separator to be a period rather than a comma. - return String.format(Locale.US, FUSIONTABLES_MAP, track.getTableId(), - latE6 / 1.E6, lonE6 / 1.E6, z); - } - - private void doUpload() { - boolean success = true; - try { - progressIndicator.setProgressValue(PROGRESS_INITIALIZATION); - progressIndicator.setProgressMessage( - context.getString(R.string.send_google_progress_reading_track)); - - // Get the track meta-data - Track track = providerUtils.getTrack(trackId); - if (track == null) { - Log.w(Constants.TAG, "Cannot get track."); - return; - } - - String originalDescription = track.getDescription(); - - // Create a new table: - progressIndicator.setProgressValue(PROGRESS_FUSION_TABLE_CREATE); - String creatingFormat = context.getString(R.string.send_google_progress_creating); - String serviceName = context.getString(SendType.FUSION_TABLES.getServiceName()); - progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName)); - if (!createNewTable(track) || !makeTableUnlisted()) { - return; - } - - progressIndicator.setProgressValue(PROGRESS_UPLOAD_DATA_MIN); - String sendingFormat = context.getString(R.string.send_google_progress_sending); - progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName)); - - // Upload all of the segments of the track plus start/end markers - if (!uploadAllTrackPoints(track, originalDescription)) { - return; - } - - progressIndicator.setProgressValue(PROGRESS_UPLOAD_WAYPOINTS); - - // Upload all the waypoints. - if (!uploadWaypoints(track)) { - return; - } - - Log.d(Constants.TAG, "SendToFusionTables: Done: " + success); - progressIndicator.setProgressValue(PROGRESS_COMPLETE); - } finally { - final boolean finalSuccess = success; - context.runOnUiThread(new Runnable() { - public void run() { - if (onCompletion != null) { - onCompletion.onSendCompleted(tableId, finalSuccess); - } - } - }); - } - } - - /** - * Creates a new table. - * If successful sets {@link #tableId}. - * - * @return true in case of success. - */ - private boolean createNewTable(Track track) { - Log.d(Constants.TAG, "Creating a new fusion table."); - String query = "CREATE TABLE '" + sqlEscape(track.getName()) + - "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; - return runUpdate(query); - } - - private boolean makeTableUnlisted() { - Log.d(Constants.TAG, "Setting visibility to unlisted."); - String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED"; - return runUpdate(query); - } - - /** - * Formats given values SQL style. Escapes single quotes with a backslash. - * - * @param values the values to format - * @return the values formatted as: ('value1','value2',...,'value_n'). - */ - private static String values(String... values) { - StringBuilder builder = new StringBuilder("("); - for (int i = 0; i < values.length; i++) { - if (i > 0) { - builder.append(','); - } - builder.append('\''); - builder.append(sqlEscape(values[i])); - builder.append('\''); - } - builder.append(')'); - return builder.toString(); - } - - private static String sqlEscape(String value) { - return value.replaceAll("'", "''"); - } - - /** - * Creates a new row representing a marker. - * - * @param name the marker name - * @param description the marker description - * @param location the marker location - * @return true in case of success. - */ - private boolean createNewPoint(String name, String description, Location location, - String marker) { - Log.d(Constants.TAG, "Creating a new row with a point."); - String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " - + values(name, description, getKmlPoint(location), marker); - return runUpdate(query); - } - - /** - * Creates a new row representing a line segment. - * - * @param track the track/segment to draw - * @return true in case of success. - */ - private boolean createNewLineString(Track track) { - Log.d(Constants.TAG, "Creating a new row with a point."); - String query = "INSERT INTO " + tableId - + " (name,description,geometry) VALUES " - + values(track.getName(), track.getDescription(), getKmlLineString(track)); - return runUpdate(query); - } - - private boolean uploadAllTrackPoints(final Track track, String originalDescription) { - SharedPreferences preferences = context.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - 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 == null || !locationsCursor.moveToFirst()) { - Log.w(Constants.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(Constants.TAG, - "Using elevation sampling factor: " + elevationSamplingFrequency - + " on " + totalLocations); - double totalDistance = 0; - - Vector distances = new Vector(); - Vector elevations = new Vector(); - DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - - List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); - Location lastLocation = null; - do { - if (totalLocationsRead % 100 == 0) { - updateTrackDataUploadProgress(); - } - - Location loc = providerUtils.createLocation(locationsCursor); - locations.add(loc); - - if (totalLocationsRead == 0) { - // Put a marker at the first point of the first valid segment: - String name = track.getName() + " " + context.getString(R.string.marker_label_start); - createNewPoint(name, "", loc, MARKER_TYPE_START); - } - - // Add to the elevation profile. - if (loc != null && LocationUtils.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) - + "

"); - String name = track.getName() + " " + context.getString(R.string.marker_label_end); - return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END); - } - - return true; - } finally { - if (locationsCursor != null) { - locationsCursor.close(); - } - } - } - - /** - * Appends the given location to the string in the format: - * longitude,latitude[,altitude] - * - * @param location the location to be added - * @param builder the string builder to use - */ - private void appendCoordinate(Location location, StringBuilder builder) { - builder - .append(location.getLongitude()) - .append(",") - .append(location.getLatitude()); - if (location.hasAltitude()) { - builder.append(","); - builder.append(location.getAltitude()); - } - } - - /** - * Gets a KML Point tag for the given location. - * - * @param location The location. - * @return the kml. - */ - private String getKmlPoint(Location location) { - StringBuilder builder = new StringBuilder(""); - appendCoordinate(location, builder); - builder.append(""); - return builder.toString(); - } - - /** - * Returns a KML LineString. - * - * @param track the track. - * @return the KML LineString. - */ - private String getKmlLineString(Track track) { - StringBuilder builder = new StringBuilder(""); - for (Location location : track.getLocations()) { - appendCoordinate(location, builder); - builder.append(' '); - } - builder.append(""); - return builder.toString(); - } - - private boolean prepareAndUploadPoints(Track track, List locations) { - updateTrackDataUploadProgress(); - - int numLocations = locations.size(); - if (numLocations < 2) { - Log.d(Constants.TAG, "Not preparing/uploading too few points"); - totalLocationsUploaded += numLocations; - return true; - } - - // Prepare/pre-process the points - ArrayList splitTracks = prepareLocations(track, locations); - - // Start uploading them - for (Track splitTrack : splitTracks) { - if (totalSegmentsUploaded > 1) { - splitTrack.setName(splitTrack.getName() + " " - + String.format( - context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded)); - } - totalSegmentsUploaded++; - Log.d(Constants.TAG, - "SendToFusionTables: Prepared feature for upload w/ " - + splitTrack.getLocations().size() + " points."); - - // Transmit tracks via GData feed: - // ------------------------------- - Log.d(Constants.TAG, - "SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth); - if (!uploadTrackPoints(splitTrack)) { - Log.e(Constants.TAG, "Uploading failed"); - return false; - } - } - - locations.clear(); - totalLocationsUploaded += numLocations; - updateTrackDataUploadProgress(); - return true; - } - - /** - * Prepares a buffer of locations for transmission to google fusion tables. - * - * @param track the original track with meta data - * @param locations locations on the track - * @return an array of tracks each with a sub section of the points in the - * original buffer - */ - private ArrayList prepareLocations( - Track track, Iterable locations) { - ArrayList splitTracks = new ArrayList(); - - // Create segments from each full track: - Track segment = new Track(); - TripStatistics segmentStats = segment.getStatistics(); - TripStatistics trackStats = track.getStatistics(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - segmentStats.setStartTime(trackStats.getStartTime()); - segmentStats.setStopTime(trackStats.getStopTime()); - boolean startNewTrackSegment = false; - for (Location loc : locations) { - if (totalLocationsPrepared % 100 == 0) { - updateTrackDataUploadProgress(); - } - if (loc.getLatitude() > 90) { - startNewTrackSegment = true; - } - - if (startNewTrackSegment) { - // Close up the last segment. - prepareTrackSegment(segment, splitTracks); - - Log.d(Constants.TAG, - "MyTracksSendToFusionTables: Starting new track segment..."); - startNewTrackSegment = false; - segment = new Track(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - } - - if (loc.getLatitude() <= 90) { - segment.addLocation(loc); - if (segmentStats.getStartTime() < 0) { - segmentStats.setStartTime(loc.getTime()); - } - } - totalLocationsPrepared++; - } - - prepareTrackSegment(segment, splitTracks); - - return splitTracks; - } - - /** - * Prepares a track segment for sending to google fusion tables. - * The main steps are: - * - correcting end time - * - decimating locations - * - splitting into smaller tracks. - * - * The final track pieces will be put in the array list splitTracks. - * - * @param segment the original segment of the track - * @param splitTracks an array of smaller track segments - */ - private void prepareTrackSegment( - Track segment, ArrayList splitTracks) { - TripStatistics segmentStats = segment.getStatistics(); - if (segmentStats.getStopTime() < 0 - && segment.getLocations().size() > 0) { - segmentStats.setStopTime(segment.getLocations().size() - 1); - } - - /* - * Decimate to 2 meter precision. Fusion tables doesn't like too many - * points: - */ - LocationUtils.decimate(segment, 2.0); - - /* If the track still has > 2500 points, split it in pieces: */ - final int maxPoints = 2500; - if (segment.getLocations().size() > maxPoints) { - splitTracks.addAll(LocationUtils.split(segment, maxPoints)); - } else if (segment.getLocations().size() >= 2) { - splitTracks.add(segment); - } - } - - private boolean uploadTrackPoints(Track splitTrack) { - int numLocations = splitTrack.getLocations().size(); - if (numLocations < 2) { - // Need at least two points for a polyline: - Log.w(Constants.TAG, "Not uploading too few points"); - return true; - } - return createNewLineString(splitTrack); - } - - /** - * Uploads all of the waypoints associated with this track to a table. - * - * @param track The track to upload waypoints for. - * - * @return True on success. - */ - private boolean uploadWaypoints(final Track track) { - // TODO: Stream through the waypoints in chunks. - // I am leaving the number of waypoints very high which should not be a - // problem because we don't try to load them into objects all at the - // same time. - boolean success = true; - Cursor c = null; - try { - c = providerUtils.getWaypointsCursor( - track.getId(), 0, - Constants.MAX_LOADED_WAYPOINTS_POINTS); - if (c != null) { - if (c.moveToFirst()) { - // This will skip the 1st waypoint (it carries the stats for the - // last segment). - while (c.moveToNext()) { - Waypoint wpt = providerUtils.createWaypoint(c); - Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint."); - success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), - MARKER_TYPE_WAYPOINT); - if (!success) { - break; - } - } - } - } - if (!success) { - Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed."); - } - return success; - } finally { - if (c != null) { - c.close(); - } - } - } - - private void updateTrackDataUploadProgress() { - // The percent of the total that represents the completed part of this - // segment. We calculate it as an absolute percentage, and then scale it - // to fit the completion percentage range alloted to track data upload. - double totalPercentage = - (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) - / (totalLocations * 3.0); - - double scaledPercentage = totalPercentage - * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; - - progressIndicator.setProgressValue((int) scaledPercentage); - } - - /** - * Runs an update query. Handles authentication. - * - * @param query The given SQL like query - * @return true in case of success - */ - private boolean runUpdate(final String query) { - GDataWrapper wrapper = new GDataWrapper(); - wrapper.setAuthManager(auth); - wrapper.setRetryOnAuthFailure(true); - wrapper.setClient(httpRequestFactory); - Log.d(Constants.TAG, "GData connection prepared: " + this.auth); - wrapper.runQuery(new QueryFunction() { - @Override - public void query(HttpRequestFactory factory) - throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException, - GDataWrapper.AuthenticationException { - GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL); - - String sql = "sql=" + URLEncoder.encode(query, "UTF-8"); - ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql)); - InputStreamContent isc = new InputStreamContent(null, inputStream ); - - HttpRequest request = factory.buildPostRequest(url, isc); - - GoogleHeaders headers = new GoogleHeaders(); - headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context)); - headers.gdataVersion = GDATA_VERSION; - headers.setGoogleLogin(auth.getAuthToken()); - headers.setContentType(CONTENT_TYPE); - request.setHeaders(headers); - - Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql); - HttpResponse response; - try { - response = request.execute(); - } catch (HttpResponseException e) { - throw new GDataWrapper.HttpException(e.getResponse().getStatusCode(), - e.getResponse().getStatusMessage()); - } - boolean success = response.isSuccessStatusCode(); - if (success) { - byte[] result = new byte[1024]; - int read = response.getContent().read(result); - String s = new String(result, 0, read, "UTF8"); - String[] lines = s.split(Strings.LINE_SEPARATOR); - if (lines[0].equals("tableid")) { - tableId = lines[1]; - Log.d(Constants.TAG, "tableId = " + tableId); - } else { - Log.w(Constants.TAG, "Unrecognized response: " + lines[0]); - } - } else { - Log.d(Constants.TAG, - "Query failed: " + response.getStatusMessage() + " (" - + response.getStatusCode() + ")"); - throw new GDataWrapper.HttpException( - response.getStatusCode(), response.getStatusMessage()); - } - } - }); - return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java index 2ceb25ca5..8d51071f0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java @@ -325,9 +325,8 @@ public class SendToMaps implements Runnable { progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName)); for (Track splitTrack : splitTracks) { if (totalSegmentsUploaded > 1) { - splitTrack.setName(splitTrack.getName() + " " - + String.format( - context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded)); + splitTrack.setName(context.getString( + R.string.send_google_track_part_label, splitTrack.getName(), totalSegmentsUploaded)); } totalSegmentsUploaded++; Log.d(TAG, diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java index 97bb07031..851930c20 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java @@ -201,9 +201,8 @@ public class MapsFacade { } else { iconUrl = END_ICON_URL; } - String title = trackName + " " - + (isStart ? context.getString(R.string.marker_label_start) - : context.getString(R.string.marker_label_end)); + String title = isStart ? context.getString(R.string.marker_label_start, trackName) : + context.getString(R.string.marker_label_end, trackName); String description = isStart ? "" : trackDescription; return buildMapsPlacemarkFeature(title, description, iconUrl, geoPoint); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java index 4c585a730..42c3a564c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java @@ -27,8 +27,6 @@ import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.AuthManager.AuthCallback; import com.google.android.apps.mytracks.io.AuthManagerFactory; import com.google.android.apps.mytracks.io.SendToDocs; -import com.google.android.apps.mytracks.io.SendToFusionTables; -import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; import com.google.android.apps.mytracks.io.SendToMaps; import com.google.android.apps.mytracks.io.maps.MapsConstants; import com.google.android.apps.mytracks.io.maps.MapsFacade; @@ -78,7 +76,6 @@ public class SendActivity extends Activity implements ProgressIndicator { SEND_TO_MAPS, SEND_TO_MAPS_DONE, AUTHENTICATE_FUSION_TABLES, - SEND_TO_FUSION_TABLES, SEND_TO_FUSION_TABLES_DONE, AUTHENTICATE_DOCS, AUTHENTICATE_TRIX, @@ -243,8 +240,6 @@ public class SendActivity extends Activity implements ProgressIndicator { return onSendToGoogleMapsDone(); case AUTHENTICATE_FUSION_TABLES: return authenticateToFusionTables(); - case SEND_TO_FUSION_TABLES: - return sendToFusionTables(); case SEND_TO_FUSION_TABLES_DONE: return onSendToFusionTablesDone(); case AUTHENTICATE_DOCS: @@ -277,8 +272,6 @@ public class SendActivity extends Activity implements ProgressIndicator { * SendToGoogleDialog. */ private SendState startSend() { - showDialog(PROGRESS_DIALOG); - if (sendToMaps) { return SendState.AUTHENTICATE_MAPS; } else if (sendToFusionTables) { @@ -305,6 +298,7 @@ public class SendActivity extends Activity implements ProgressIndicator { private SendState authenticateToGoogleMaps() { Log.d(TAG, "SendActivity.authenticateToGoogleMaps"); + showDialog(PROGRESS_DIALOG); progressDialog.setProgress(0); progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.MAPS)); authenticate(Constants.AUTHENTICATE_TO_MAPS, MapsConstants.SERVICE_NAME); @@ -379,48 +373,11 @@ public class SendActivity extends Activity implements ProgressIndicator { } private SendState authenticateToFusionTables() { - progressDialog.setProgress(0); - progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.FUSION_TABLES)); - authenticate(Constants.AUTHENTICATE_TO_FUSION_TABLES, SendToFusionTables.SERVICE_ID); - // AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables - return SendState.NOT_READY; - } - - private SendState sendToFusionTables() { tracker.trackPageView("/send/fusion_tables"); - - OnSendCompletedListener onCompletion = new OnSendCompletedListener() { - @Override - public void onSendCompleted(String tableId, boolean success) { - sendToFusionTablesSuccess = success; - if (sendToFusionTablesSuccess) { - sendToFusionTablesTableId = tableId; - // Update the table id for this track: - try { - Track track = providerUtils.getTrack(sendTrackId); - if (track != null) { - track.setTableId(tableId); - providerUtils.updateTrack(track); - } else { - Log.w(TAG, "Updating table id failed."); - } - } catch (RuntimeException e) { - // If that fails whatever reasons we'll just log an error, but - // continue. - Log.w(TAG, "Updating table id failed.", e); - } - } - - executeStateMachine(SendState.SEND_TO_FUSION_TABLES_DONE); - } - }; - - final SendToFusionTables sender = new SendToFusionTables( - this, getAuthManager(SendToFusionTables.SERVICE_ID), sendTrackId, - this /* progressIndicator */, onCompletion); - - new Thread(sender, "SendToFusionTables").start(); - + Intent intent = new Intent(this, SendFusionTablesActivity.class) + .putExtra(SendFusionTablesActivity.ACCOUNT, account) + .putExtra(SendFusionTablesActivity.TRACK_ID, sendTrackId); + startActivityForResult(intent, Constants.SEND_FUSION_TABLES); return SendState.NOT_READY; } @@ -433,6 +390,7 @@ public class SendActivity extends Activity implements ProgressIndicator { } private SendState authenticateToGoogleDocs() { + showDialog(PROGRESS_DIALOG); setProgressValue(0); setProgressMessage(getAuthenticatingProgressMessage(SendType.DOCS)); authenticate(Constants.AUTHENTICATE_TO_DOCLIST, SendToDocs.GDATA_SERVICE_NAME_DOCLIST); @@ -483,7 +441,7 @@ public class SendActivity extends Activity implements ProgressIndicator { ? MapsFacade.buildMapUrl(sendToMapsMapId) : null; String fusionTablesUrl = sendToFusionTables && sendToFusionTablesSuccess - ? getFusionTablesUrl(sendTrackId) + ? SendFusionTablesUtils.getMapUrl(providerUtils.getTrack(sendTrackId)) : null; Intent intent = new Intent(this, UploadResultActivity.class) .putExtra(UploadResultActivity.HAS_MAPS_RESULT, sendToMaps) @@ -504,12 +462,7 @@ public class SendActivity extends Activity implements ProgressIndicator { return SendState.DONE; } - protected String getFusionTablesUrl(long trackId) { - Track track = providerUtils.getTrack(trackId); - return SendToFusionTables.getMapVisualizationUrl(track); - } - - /** + /** * Initializes the authentication manager which obtains an authentication * token, prompting the user for a login and password if needed. */ @@ -560,6 +513,17 @@ public class SendActivity extends Activity implements ProgressIndicator { nextState = SendState.START; break; } + case Constants.SEND_FUSION_TABLES: { + if (resultCode == RESULT_CANCELED) { + nextState = SendState.FINISH; + break; + } + sendToFusionTablesSuccess = results.getBooleanExtra( + SendFusionTablesActivity.SUCCESS, false); + sendToFusionTablesTableId = results.getStringExtra(SendFusionTablesActivity.TABLE_ID); + nextState = SendState.SEND_TO_FUSION_TABLES_DONE; + break; + } default: { Log.e(TAG, "Unrequested result: " + requestCode); return; @@ -578,10 +542,6 @@ public class SendActivity extends Activity implements ProgressIndicator { // Authenticated with Google Maps nextState = SendState.PICK_MAP; break; - case Constants.AUTHENTICATE_TO_FUSION_TABLES: - // Authenticated with Google Fusion Tables - nextState = SendState.SEND_TO_FUSION_TABLES; - break; case Constants.AUTHENTICATE_TO_DOCLIST: // Authenticated with Google Docs nextState = SendState.AUTHENTICATE_TRIX; @@ -612,6 +572,7 @@ public class SendActivity extends Activity implements ProgressIndicator { } return authManager; } + /** * Resets status information for sending to Maps/Fusion Tables/Docs. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java new file mode 100644 index 000000000..1be368908 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java @@ -0,0 +1,135 @@ +/* + * 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.sendtogoogle; + +import com.google.android.maps.mytracks.R; + +import android.accounts.Account; +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; + +/** + * An activity to send a track to Google Fusion Tables. + *

+ * The activity gets recreated when the screen rotates. To support the activity + * displaying a progress dialog, we do the following: + *

    + *
  • use one instance of an AyncTask to send the track
  • + *
  • save that instance as the last non configuration instance of the activity + *
  • + *
  • when a new activity is created, pass the activity to the AsyncTask so + * that the AsyncTask can update the progress dialog of the activity
  • + *
+ * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesActivity extends Activity { + + // parameters in the input intent + public static final String ACCOUNT = "account"; + public static final String TRACK_ID = "trackId"; + + // parameters in the output intent + public static final String SUCCESS = "success"; + public static final String TABLE_ID = "tableId"; + + private static final int PROGRESS_DIALOG = 1; + + private SendFusionTablesAsyncTask asyncTask; + private ProgressDialog progressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Account account = intent.getParcelableExtra(ACCOUNT); + long trackId = intent.getLongExtra(TRACK_ID, -1L); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof SendFusionTablesAsyncTask) { + asyncTask = (SendFusionTablesAsyncTask) retained; + asyncTask.setActivity(this); + } else { + asyncTask = new SendFusionTablesAsyncTask(this, account, trackId); + asyncTask.execute(); + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + asyncTask.setActivity(null); + return asyncTask; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case PROGRESS_DIALOG: + progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(getString( + R.string.send_google_progress_title, getString(R.string.send_google_fusion_tables))); + progressDialog.setMax(100); + progressDialog.setProgress(0); + progressDialog.setCancelable(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + asyncTask.cancel(true); + setResult(RESULT_CANCELED); + finish(); + } + }); + return progressDialog; + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + * @param tableId tableId if available + */ + public void onAsyncTaskCompleted(boolean success, String tableId) { + Intent intent = new Intent().putExtra(SUCCESS, success).putExtra(TABLE_ID, tableId); + setResult(RESULT_OK, intent); + finish(); + } + + /** + * Shows the progress dialog. + */ + public void showProgressDialog() { + showDialog(PROGRESS_DIALOG); + } + + /** + * Sets the progress dialog value. + */ + public void setProgressDialogValue(int value) { + if (progressDialog != null) { + progressDialog.setProgress(value); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java new file mode 100644 index 000000000..e243ba684 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java @@ -0,0 +1,617 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.apps.mytracks.Constants; +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.ApiFeatures; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.SystemUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; +import com.google.api.client.googleapis.GoogleHeaders; +import com.google.api.client.googleapis.MethodOverride; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.util.Strings; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.location.Location; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +/** + * AsyncTask to send a track to Google Fusion Tables. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesAsyncTask extends AsyncTask { + + private static final String APP_NAME_PREFIX = "Google-My-Tracks-"; + private static final String SQL_KEY = "sql="; + private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; + private static final String SERVICE_ID = "fusiontables"; + private static final String FUSION_TABLES_BASE_URL = + "https://www.google.com/fusiontables/api/query"; + private static final int MAX_POINTS_PER_UPLOAD = 2048; + private static final String GDATA_VERSION = "2"; + + private static final int PROGRESS_CREATE_TABLE = 0; + private static final int PROGRESS_UNLIST_TABLE = 5; + private static final int PROGRESS_UPLOAD_DATA_MIN = 10; + private static final int PROGRESS_UPLOAD_DATA_MAX = 90; + private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; + private static final int PROGRESS_COMPLETE = 100; + + // See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991 + private static final String MARKER_TYPE_START = "large_green"; + private static final String MARKER_TYPE_END = "large_red"; + private static final String MARKER_TYPE_WAYPOINT = "large_yellow"; + + private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName(); + + private SendFusionTablesActivity activity; + + private final Context context; + private final Account account; + private final long trackId; + private final MyTracksProviderUtils myTracksProviderUtils; + private final HttpRequestFactory httpRequestFactory; + + /** + * True if can retry sending to Google Fusion Tables. + */ + private boolean canRetry; + + /** + * True if the AsyncTask has completed. + */ + private boolean completed; + + // The following variables are for per upload states + private String authToken; + private boolean success; + private String tableId; + int currentSegment; + + public SendFusionTablesAsyncTask( + SendFusionTablesActivity activity, Account account, long trackId) { + this.activity = activity; + this.account = account; + this.trackId = trackId; + + context = activity.getApplicationContext(); + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); + HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport(); + httpRequestFactory = transport.createRequestFactory(new MethodOverride()); + + canRetry = true; + completed = false; + } + + /** + * Sets the activity associated with this AyncTask. + * + * @param activity the activity. + */ + public void setActivity(SendFusionTablesActivity activity) { + this.activity = activity; + if (completed) { + activity.onAsyncTaskCompleted(success, tableId); + } + } + + @Override + protected void onPreExecute() { + activity.showProgressDialog(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return doUpload(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (activity != null) { + activity.setProgressDialogValue(values[0]); + } + } + + @Override + protected void onPostExecute(Boolean result) { + completed = true; + if (success) { + Track track = myTracksProviderUtils.getTrack(trackId); + if (track != null) { + track.setTableId(tableId); + myTracksProviderUtils.updateTrack(track); + } else { + Log.d(TAG, "No track"); + } + } + if (activity != null) { + activity.onAsyncTaskCompleted(success, tableId); + } + } + + /** + * Uploads a track to Google Fusion Tables. + * + * @return true if success. + */ + private boolean doUpload() { + // Reset the per upload states + authToken = null; + success = false; + tableId = null; + currentSegment = 1; + + try { + authToken = AccountManager.get(context).blockingGetAuthToken(account, SERVICE_ID, false); + } catch (OperationCanceledException e) { + return retryUpload(); + } catch (AuthenticatorException e) { + return retryUpload(); + } catch (IOException e) { + return retryUpload(); + } + + Track track = myTracksProviderUtils.getTrack(trackId); + if (track == null) { + Log.d(TAG, "Track is null"); + return false; + } + + // Create a new table + publishProgress(PROGRESS_CREATE_TABLE); + if (!createNewTable(track)) { + // Retry upload in case the auth token is invalid + return retryUpload(); + } + + // Unlist table + publishProgress(PROGRESS_UNLIST_TABLE); + if (!unlistTable()) { + return false; + } + + // Upload all the track points plus the start and end markers + publishProgress(PROGRESS_UPLOAD_DATA_MIN); + if (!uploadAllTrackPoints(track)) { + return false; + } + + // Upload all the waypoints + publishProgress(PROGRESS_UPLOAD_WAYPOINTS); + if (!uploadWaypoints()) { + return false; + } + + publishProgress(PROGRESS_COMPLETE); + return true; + } + + /** + * Retries upload. Invalidates the authToken. If can retry, invokes + * {@link SendFusionTablesAsyncTask#doUpload()}. Returns false if cannot + * retry. + */ + private boolean retryUpload() { + if (isCancelled()) { + return false; + } + + AccountManager.get(context).invalidateAuthToken(SERVICE_ID, authToken); + if (canRetry) { + canRetry = false; + return doUpload(); + } + return false; + } + + /** + * Creates a new table. + * + * @param track the track + * @return true if success. + */ + private boolean createNewTable(Track track) { + String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName()) + + "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; + return sendQuery(query, true); + } + + /** + * Unlists a table. + * + * @return true if success. + */ + private boolean unlistTable() { + String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED"; + return sendQuery(query, false); + } + + /** + * Uploads all the points in a track. + * + * @param track the track + * @return true if success. + */ + private boolean uploadAllTrackPoints(Track track) { + Cursor locationsCursor = null; + try { + SharedPreferences prefs = context.getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true); + + locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false); + if (locationsCursor == null) { + Log.d(TAG, "Location cursor is null"); + return false; + } + int locationsCount = locationsCursor.getCount(); + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); + Location lastLocation = null; + + // Limit the number of elevation readings. Ideally we would want around + // 250. + int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0)); + double totalDistance = 0; + DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); + Vector distances = new Vector(); + Vector elevations = new Vector(); + + for (int i = 0; i < locationsCount; i++) { + locationsCursor.moveToPosition(i); + + Location location = myTracksProviderUtils.createLocation(locationsCursor); + locations.add(location); + + int readCount = i + 1; + + if (readCount == 1) { + // Create a start marker + String name = context.getString(R.string.marker_label_start, track.getName()); + if (!createNewPoint(name, "", location, MARKER_TYPE_START)) { + Log.d(TAG, "Unable to create the start marker"); + return false; + } + } + + // Add to the distances and elevations vectors + if (LocationUtils.isValidLocation(location)) { + // All points go into the smoothing buffer + elevationBuffer.setNext(metricUnits ? location.getAltitude() + : location.getAltitude() * UnitConversions.M_TO_FT); + if (lastLocation != null) { + totalDistance += (double) lastLocation.distanceTo(location); + } + if (readCount % elevationSamplingFrequency == 0) { + distances.add(totalDistance); + elevations.add(elevationBuffer.getAverage()); + } + } + + // Upload periodically + if (readCount % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD) { + if (!prepareAndUploadPoints(track, locations, false)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + updateProgress(readCount, locationsCount); + locations.clear(); + } + lastLocation = location; + } + + // Do a final upload with the remaining locations + if (!prepareAndUploadPoints(track, locations, true)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + + // Create an end marker + if (lastLocation != null) { + StringUtils stringUtils = new StringUtils(context); + track.setDescription("

" + track.getDescription() + "

" + + stringUtils.generateTrackDescription(track, distances, elevations) + "

"); + String name = context.getString(R.string.marker_label_end, track.getName()); + if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) { + Log.d(TAG, "Unable to create the end marker"); + return false; + } + } + + return true; + } finally { + if (locationsCursor != null) { + locationsCursor.close(); + } + } + } + + /** + * Prepares and uploads a list of locations from a track. + * + * @param track the track + * @param locations the locations from the track + * @param lastBatch true if it is the last batch of locations + */ + private boolean prepareAndUploadPoints(Track track, List locations, boolean lastBatch) { + int numLocations = locations.size(); + if (numLocations < 2) { + Log.d(TAG, "Not preparing/uploading, too few points"); + return true; + } + + // Prepare locations + ArrayList splitTracks = prepareLocations(track, locations); + + boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1; + + // Upload segments + for (Track splitTrack : splitTracks) { + if (!onlyOneSegment) { + splitTrack.setName(context.getString( + R.string.send_google_track_part_label, splitTrack.getName(), currentSegment)); + } + if (!createNewLineString(splitTrack)) { + Log.d(TAG, "Upload points failed"); + return false; + } + currentSegment++; + } + return true; + } + + /** + * Prepares a list of locations to send to Google Fusion Tables. + * Splits the locations into segments if necessary. + * + * @param track the track + * @param locations the list of locations + * @return an array of split segments. + */ + private ArrayList prepareLocations(Track track, List locations) { + ArrayList splitTracks = new ArrayList(); + + // Create a new segment + Track segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(""); + segment.setCategory(track.getCategory()); + + TripStatistics segmentStats = segment.getStatistics(); + TripStatistics trackStats = track.getStatistics(); + segmentStats.setStartTime(trackStats.getStartTime()); + segmentStats.setStopTime(trackStats.getStopTime()); + boolean startNewTrackSegment = false; + for (Location loc : locations) { + // Latitude is greater than 90 if the location is invalid. Do not add to the + // segment. + if (loc.getLatitude() > 90) { + startNewTrackSegment = true; + } + + if (startNewTrackSegment) { + // Close the last segment + prepareTrackSegment(segment, splitTracks); + + startNewTrackSegment = false; + segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(""); + segment.setCategory(track.getCategory()); + segmentStats = segment.getStatistics(); + } + + if (loc.getLatitude() <= 90) { + segment.addLocation(loc); + + // For a new segment, sets its start time using the first available + // location time. + if (segmentStats.getStartTime() < 0) { + segmentStats.setStartTime(loc.getTime()); + } + segmentStats.setStopTime(loc.getTime()); + } + } + + prepareTrackSegment(segment, splitTracks); + + return splitTracks; + } + + /** + * Prepares a track segment for sending to Google Fusion Tables. The main + * steps are: + *
    + *
  • set the stop time
  • + *
  • decimate locations precision
  • + *
+ * The prepared track will be added to the splitTracks. + * + * @param segment the track segment + * @param splitTracks an array of track segments + */ + private void prepareTrackSegment(Track segment, ArrayList splitTracks) { + // For a new segment, sets it stop time + TripStatistics segmentStats = segment.getStatistics(); + ArrayList locations = segment.getLocations(); + if (segmentStats.getStopTime() < 0 && locations.size() > 0) { + Location lastLocation = locations.get(locations.size() - 1); + segmentStats.setStopTime(lastLocation.getTime()); + } + + // Decimate to 2 meter precision. Fusion Tables doesn't like too many + // points. + LocationUtils.decimate(segment, 2.0); + splitTracks.add(segment); + } + + /** + * Uploads all the waypoints. + * + * @return true if success. + */ + private boolean uploadWaypoints() { + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointsCursor( + trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS); + if (cursor != null && cursor.moveToFirst()) { + // This will skip the first waypoint (it carries the stats for the + // track). + while (cursor.moveToNext()) { + Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor); + if (!createNewPoint( + wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) { + Log.d(TAG, "Upload waypoints failed"); + return false; + } + } + } + return true; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Creates a new row in Google Fusion Tables representing a marker as a + * point. + * + * @param name the marker name + * @param description the marker description + * @param location the marker location + * @param type the marker type + * @return true if success. + */ + private boolean createNewPoint( + String name, String description, Location location, String type) { + String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " + + SendFusionTablesUtils.formatSqlValues( + name, description, SendFusionTablesUtils.getKmlPoint(location), type); + return sendQuery(query, false); + } + + /** + * Creates a new row in Google Fusion Tables representing the track as a + * line segment. + * + * @param track the track + * @return true if success. + */ + private boolean createNewLineString(Track track) { + String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + + SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(), + SendFusionTablesUtils.getKmlLineString(track.getLocations())); + return sendQuery(query, false); + } + + /** + * Sends a query to Google Fusion Tables. + * + * @param query the Fusion Tables SQL query + * @param setTableId true to set the table id + * @return true if success. + */ + private boolean sendQuery(String query, boolean setTableId) { + Log.d(TAG, "SendQuery: " + query); + + if (isCancelled()) { + return false; + } + + GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL); + String sql = SQL_KEY + URLEncoder.encode(query); + ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql)); + InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream); + HttpRequest request; + try { + request = httpRequestFactory.buildPostRequest(url, inputStreamContent); + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + return false; + } + + GoogleHeaders headers = new GoogleHeaders(); + headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context)); + headers.gdataVersion = GDATA_VERSION; + headers.setGoogleLogin(authToken); + headers.setContentType(CONTENT_TYPE); + request.setHeaders(headers); + + HttpResponse response; + try { + response = request.execute(); + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + return false; + } + success = response.isSuccessStatusCode(); + if (success) { + InputStream content; + try { + content = response.getContent(); + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + return false; + } + if (setTableId) { + tableId = SendFusionTablesUtils.getTableId(content); + if (tableId == null) { + Log.d(TAG, "tableId is null"); + return false; + } + } + } else { + Log.d(TAG, + "sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode()); + return false; + } + return true; + } + + /** + * Updates the progress based on the number of locations uploaded. + * + * @param uploaded the number of uploaded locations + * @param total the number of total locations + */ + private void updateProgress(int uploaded, int total) { + double totalPercentage = uploaded / total; + double scaledPercentage = totalPercentage + * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; + publishProgress((int) scaledPercentage); + } +} + diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtils.java new file mode 100644 index 000000000..c65870a36 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtils.java @@ -0,0 +1,195 @@ +/* + * 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.sendtogoogle; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.api.client.util.Strings; +import com.google.common.annotations.VisibleForTesting; + +import android.location.Location; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Utilities for sending a track to Google Fusion Tables. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesUtils { + + private static final String UTF8 = "UTF8"; + private static final String TABLE_ID = "tableid"; + private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?" + + "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2"; + private static final String TAG = SendFusionTablesUtils.class.getSimpleName(); + + private SendFusionTablesUtils() {} + + /** + * Gets the url to visualize a fusion table on a map. + * + * @param track the track + * @return the url. + */ + public static String getMapUrl(Track track) { + if (track == null || track.getStatistics() == null || track.getTableId() == null) { + Log.e(TAG, "Invalid track."); + return null; + } + + // TODO(jshih): Determine the correct bounding box and zoom level that + // will show the entire track. + TripStatistics stats = track.getStatistics(); + double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2; + double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2; + int z = 15; + + // We explicitly format with Locale.US because we need the latitude and + // longitude to be formatted in a locale-independent manner. Specifically, + // we need the decimal separator to be a period rather than a comma. + return String.format( + Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z); + } + + /** + * Formats an array of values as a SQL VALUES like + * ('value1','value2',...,'value_n'). Escapes single quotes with two single + * quotes. + * + * @param values an array of values to format + * @return the formated SQL VALUES. + */ + public static String formatSqlValues(String... values) { + StringBuilder builder = new StringBuilder("("); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + builder.append(','); + } + builder.append('\''); + builder.append(escapeSqlString(values[i])); + builder.append('\''); + } + builder.append(")"); + return builder.toString(); + } + + /** + * Escapes a SQL string. Escapes single quotes with two single quotes. + * + * @param string the string + * @return the escaped string. + */ + public static String escapeSqlString(String string) { + return string.replaceAll("'", "''"); + } + + /** + * Gets a KML Point value representing a location. + * + * @param location the location + * @return the KML Point value. + */ + public static String getKmlPoint(Location location) { + StringBuilder builder = new StringBuilder(""); + if (location != null) { + appendLocation(location, builder); + } + builder.append(""); + return builder.toString(); + } + + /** + * Gets a KML LineString value representing an array of locations. + * + * @param locations the locations. + * @return the KML LineString value. + */ + public static String getKmlLineString(ArrayList locations) { + StringBuilder builder = new StringBuilder(""); + if (locations != null) { + for (int i = 0; i < locations.size(); i++) { + if (i != 0) { + builder.append(' '); + } + appendLocation(locations.get(i), builder); + } + } + builder.append(""); + return builder.toString(); + } + + /** + * Appends a location to a string builder using "longitude,latitude[,altitude]" format. + * + * @param location the location + * @param builder the string builder + */ + @VisibleForTesting + static void appendLocation(Location location, StringBuilder builder) { + builder.append(location.getLongitude()).append(",").append(location.getLatitude()); + if (location.hasAltitude()) { + builder.append(","); + builder.append(location.getAltitude()); + } + } + + /** + * Gets the table id from an input streawm. + * + * @param inputStream input stream + * @return table id or null if not available. + */ + public static String getTableId(InputStream inputStream) { + if (inputStream == null) { + Log.d(TAG, "inputStream is null"); + return null; + } + byte[] result = new byte[1024]; + int read; + try { + read = inputStream.read(result); + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + return null; + } + if (read == -1) { + Log.d(TAG, "no data read"); + return null; + } + String s; + try { + s = new String(result, 0, read, UTF8); + } catch (UnsupportedEncodingException e) { + Log.d(TAG, e.getMessage()); + return null; + } + + String[] lines = s.split(Strings.LINE_SEPARATOR); + if (lines.length > 1 && lines[0].equals(TABLE_ID)) { + // returns the next line + return lines[1]; + } else { + Log.d(TAG, "Response is not valid: " + s); + return null; + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtilsTest.java new file mode 100644 index 000000000..7b2bebea6 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesUtilsTest.java @@ -0,0 +1,285 @@ +/* + * 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.sendtogoogle; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatistics; + +import android.location.Location; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; + +import junit.framework.TestCase; + +/** + * Tests {@link SendFusionTablesUtils}. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesUtilsTest extends TestCase { + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null track. + */ + public void testGetMapUrl_null_track() { + assertEquals(null, SendFusionTablesUtils.getMapUrl(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null table id. + */ + public void testGetMapUrl_null_table_id() { + Track track = new Track(); + TripStatistics stats = new TripStatistics(); + stats.setBounds((int) 100.E6, (int) 10.E6, (int) 50.E6, (int) 5.E6); + track.setStatistics(stats); + track.setTableId(null); + assertEquals(null, SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null stats. + */ + public void testGetMapUrl_null_stats() { + Track track = new Track(); + track.setStatistics(null); + track.setTableId("123"); + assertEquals(null, SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with a valid track. + */ + public void testGetMapUrl_valid_track() { + Track track = new Track(); + TripStatistics stats = new TripStatistics(); + stats.setBounds((int) 100.E6, (int) 10.E6, (int) 50.E6, (int) 5.E6); + track.setStatistics(stats); + track.setTableId("123"); + assertEquals( + "https://www.google.com/fusiontables/embedviz?" + + "viz=MAP&q=select+col0,+col1,+col2,+col3+from+123+&h=false&lat=7.500000&lng=75.000000" + + "&z=15&t=1&l=col2", SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * no value. + */ + public void testFormatSqlValues_no_value() { + assertEquals("()", SendFusionTablesUtils.formatSqlValues()); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * an empty string. + */ + public void testFormatSqlValues_empty_string() { + assertEquals("(\'\')", SendFusionTablesUtils.formatSqlValues("")); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * one value. + */ + public void testFormatSqlValues_one_value() { + assertEquals("(\'first\')", SendFusionTablesUtils.formatSqlValues("first")); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * two values. + */ + public void testFormatSqlValues_two_values() { + assertEquals( + "(\'first\',\'second\')", SendFusionTablesUtils.formatSqlValues("first", "second")); + } + + /** + * Tests {@link SendFusionTablesUtils#escapeSqlString(String)} with a value + * containing several single quotes. + */ + public void testEscapeSqValuel() { + String value = "let's'"; + assertEquals("let''s''", SendFusionTablesUtils.escapeSqlString(value)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlPoint(Location)} with a null + * point. + */ + public void testGetKmlPoint_null_point() { + assertEquals( + "", SendFusionTablesUtils.getKmlPoint(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlPoint(Location)} with a valid + * point. + */ + public void testGetKmlPoint_valid_point() { + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + assertEquals("10.1,20.2,30.3", + SendFusionTablesUtils.getKmlPoint(location)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with a null + * locations. + */ + public void testKmlLineString_null_locations() { + assertEquals("", + SendFusionTablesUtils.getKmlLineString(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with no + * location. + */ + public void testKmlLineString_no_location() { + ArrayList locations = new ArrayList(); + assertEquals("", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with one + * location. + */ + public void testKmlLineString_one_location() { + ArrayList locations = new ArrayList(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + locations.add(location); + assertEquals("10.1,20.2,30.3", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with two + * locations. + */ + public void testKmlLineString_two_locations() { + ArrayList locations = new ArrayList(); + Location location1 = new Location("test"); + location1.setLongitude(10.1); + location1.setLatitude(20.2); + location1.setAltitude(30.3); + locations.add(location1); + + Location location2 = new Location("test"); + location2.setLongitude(1.1); + location2.setLatitude(2.2); + location2.removeAltitude(); + locations.add(location2); + + assertEquals("10.1,20.2,30.3 1.1,2.2", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#appendLocation(Location, StringBuilder)} + * with no altitude. + */ + public void testAppendLocation_no_altitude() { + StringBuilder builder = new StringBuilder(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.removeAltitude(); + SendFusionTablesUtils.appendLocation(location, builder); + assertEquals("10.1,20.2", builder.toString()); + } + + /** + * Tests {@link SendFusionTablesUtils#appendLocation(Location, StringBuilder)} + * with altitude. + */ + public void testAppendLocation_has_altitude() { + StringBuilder builder = new StringBuilder(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + SendFusionTablesUtils.appendLocation(location, builder); + assertEquals("10.1,20.2,30.3", builder.toString()); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with a null + * inputstream. + */ + public void testGetTableId_null() { + assertEquals(null, SendFusionTablesUtils.getTableId(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an + * inputstream containing no data. + */ + public void testGetTableId_no_data() { + InputStream inputStream = new ByteArrayInputStream(new byte[0]); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an empty + * inputstream. + */ + public void testGetTableId_empty() { + String string = ""; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an one + * line inputstream. + */ + public void testGetTableId_one_line() { + String string = "tableid"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an + * inputstream not containing "tableid". + */ + public void testGetTableId_no_table_id() { + String string = "error\n123"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with a valid + * inputstream. + */ + public void testGetTableId() { + String string = "tableid\n123"; + InputStream inputStream = null; + inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals("123", SendFusionTablesUtils.getTableId(inputStream)); + } +} \ No newline at end of file