/*
* Copyright (c) 2013, Will Szumski
* Copyright (c) 2013, Doug Szumski
*
* This file is part of Cyclismo.
*
* Cyclismo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cyclismo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cyclismo. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* 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 org.cowboycoders.cyclismo.io.docs;
import android.content.Context;
import android.util.Log;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import org.cowboycoders.cyclismo.R;
import org.cowboycoders.cyclismo.content.Track;
import org.cowboycoders.cyclismo.io.fusiontables.SendFusionTablesUtils;
import org.cowboycoders.cyclismo.io.gdata.docs.DocumentsClient;
import org.cowboycoders.cyclismo.io.gdata.docs.SpreadsheetsClient;
import org.cowboycoders.cyclismo.io.gdata.docs.SpreadsheetsClient.WorksheetEntry;
import org.cowboycoders.cyclismo.io.maps.SendMapsUtils;
import org.cowboycoders.cyclismo.stats.TripStatistics;
import org.cowboycoders.cyclismo.util.PreferencesUtils;
import org.cowboycoders.cyclismo.util.ResourceUtils;
import org.cowboycoders.cyclismo.util.StringUtils;
import org.cowboycoders.cyclismo.util.UnitConversions;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Utilities for sending a track to Google Docs.
*
* @author Sandor Dornbush
* @author Matthew Simmons
*/
public class SendDocsUtils {
@VisibleForTesting
public static final String GET_SPREADSHEET_BY_TITLE_URI =
"https://docs.google.com/feeds/documents/private/full?"
+ "category=mine,spreadsheet&title=%s&title-exact=true";
private static final String CREATE_SPREADSHEET_URI =
"https://docs.google.com/feeds/documents/private/full";
private static final String GET_WORKSHEETS_URI =
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
@VisibleForTesting
public static final String GET_WORKSHEET_URI =
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
@VisibleForTesting
public static final String SPREADSHEET_ID_PREFIX =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
@VisibleForTesting
public static final String CONTENT_TYPE = "Content-Type";
@VisibleForTesting
public static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
@VisibleForTesting
public static final String AUTHORIZATION = "Authorization";
@VisibleForTesting
public static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
private static final String SLUG = "Slug";
// Google Docs can only parse numbers in the English locale.
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH);
private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance(
Locale.ENGLISH);
static {
NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setMinimumFractionDigits(2);
}
private static final String TAG = SendDocsUtils.class.getSimpleName();
private SendDocsUtils() {}
/**
* Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet
* doesn't exist.
*
* @param title the title of the spreadsheet
* @param documentsClient the documents client
* @param authToken the auth token
* @return spreadsheet id or null if it doesn't exist.
*/
public static String getSpreadsheetId(
String title, DocumentsClient documentsClient, String authToken)
throws IOException, ParseException, HttpException {
GDataParser gDataParser = null;
try {
String uri = String.format(
Locale.US, GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title, "UTF-8"));
gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
Entry entry = gDataParser.readNextEntry(null);
String entryTitle = entry.getTitle();
if (entryTitle.equals(title)) {
return getEntryId(entry);
}
}
return null;
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
}
/**
* Gets the id from an entry. Returns null if not available.
*
* @param entry the entry
*/
@VisibleForTesting
static String getEntryId(Entry entry) {
String entryId = entry.getId();
if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) {
return entryId.substring(SPREADSHEET_ID_PREFIX.length());
}
return null;
}
/**
* Creates a new spreadsheet with the given title. Returns the spreadsheet ID
* if successful. Returns null otherwise. Note that it is possible that a new
* spreadsheet is created, but the returned ID is null.
*
* @param title the title
* @param authToken the auth token
* @param context the context
*/
public static String createSpreadsheet(String title, String authToken, Context context)
throws IOException {
URL url = new URL(CREATE_SPREADSHEET_URI);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
conn.addRequestProperty(SLUG, title);
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
conn.setDoOutput(true);
OutputStream outputStream = conn.getOutputStream();
ResourceUtils.readBinaryFileToOutputStream(
context, R.raw.mytracks_empty_spreadsheet, outputStream);
// Get the response
BufferedReader bufferedReader = null;
StringBuilder resultBuilder = new StringBuilder();
try {
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
resultBuilder.append(line);
}
} catch (FileNotFoundException e) {
// The GData API sometimes throws an error, even though creation of
// the document succeeded. In that case let's just return. The caller
// then needs to check if the doc actually exists.
Log.d(TAG, "Unable to read result after creating a spreadsheet", e);
return null;
} finally {
outputStream.close();
if (bufferedReader != null) {
bufferedReader.close();
}
}
return getNewSpreadsheetId(resultBuilder.toString());
}
/**
* Gets the spreadsheet id from a create spreadsheet result.
*
* @param result the create spreadsheet result
*/
@VisibleForTesting
static String getNewSpreadsheetId(String result) {
int idTagIndex = result.indexOf("<id>");
if (idTagIndex == -1) {
return null;
}
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
if (idTagCloseIndex == -1) {
return null;
}
int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex);
if (idStringStart == -1) {
return null;
}
return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex);
}
/**
* Gets the first worksheet ID of a spreadsheet. Returns null if not
* available.
*
* @param spreadsheetId the spreadsheet ID
* @param spreadsheetClient the spreadsheet client
* @param authToken the auth token
*/
public static String getWorksheetId(
String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken)
throws IOException, AuthenticationException, ParseException {
GDataParser gDataParser = null;
try {
String uri = String.format(Locale.US, GET_WORKSHEETS_URI, spreadsheetId);
gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken);
gDataParser.init();
if (!gDataParser.hasMoreData()) {
Log.d(TAG, "No worksheet");
return null;
}
// Get the first worksheet
WorksheetEntry worksheetEntry =
(WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry());
return getWorksheetEntryId(worksheetEntry);
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
}
/**
* Gets the worksheet id from a worksheet entry. Returns null if not available.
*
* @param entry the worksheet entry
*/
@VisibleForTesting
static String getWorksheetEntryId(WorksheetEntry entry) {
String id = entry.getId();
int lastSlash = id.lastIndexOf('/');
if (lastSlash == -1) {
Log.d(TAG, "No id");
return null;
}
return id.substring(lastSlash + 1);
}
/**
* Adds a track's info as a row in a worksheet.
*
* @param track the track
* @param spreadsheetId the spreadsheet ID
* @param worksheetId the worksheet ID
* @param authToken the auth token
* @param context the context
*/
public static void addTrackInfo(
Track track, String spreadsheetId, String worksheetId, String authToken, Context context)
throws IOException {
String worksheetUri = String.format(Locale.US, GET_WORKSHEET_URI, spreadsheetId, worksheetId);
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken);
}
/**
* Gets the row content containing the track's info.
*
* @param track the track
* @param metricUnits true to use metric
* @param context the context
*/
@VisibleForTesting
static String getRowContent(Track track, boolean metricUnits, Context context) {
TripStatistics stats = track.getTripStatistics();
String distanceUnit = context.getString(
metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
String speedUnit = context.getString(
metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
String elevationUnit = context.getString(
metricUnits ? R.string.unit_meter : R.string.unit_feet);
StringBuilder builder = new StringBuilder().append("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>");
appendTag(builder, "name", track.getName());
appendTag(builder, "description", track.getDescription());
appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
appendTag(builder, "totaltime", StringUtils.formatElapsedTimeWithHour(stats.getTotalTime()));
appendTag(builder, "movingtime", StringUtils.formatElapsedTimeWithHour(stats.getMovingTime()));
appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
appendTag(builder, "distanceunit", distanceUnit);
appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits));
appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits));
appendTag(builder, "speedunit", speedUnit);
appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits));
appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits));
appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits));
appendTag(builder, "elevationunit", elevationUnit);
String map = SendMapsUtils.getMapUrl(track);
if (map == null) {
map = context.getString(R.string.value_unknown);
}
appendTag(builder, "map", map);
String fusionTable = SendFusionTablesUtils.getMapUrl(track);
if (fusionTable == null) {
fusionTable = context.getString(R.string.value_unknown);
}
appendTag(builder, "fusiontable", fusionTable);
builder.append("</entry>");
return builder.toString();
}
/**
* Appends a name-value pair as a gsx tag to a string builder.
*
* @param stringBuilder the string builder
* @param name the name
* @param value the value
*/
@VisibleForTesting
static void appendTag(StringBuilder stringBuilder, String name, String value) {
stringBuilder
.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.formatCData(value))
.append("</gsx:")
.append(name)
.append(">");
}
/**
* Gets the distance. Performs unit conversion and formatting.
*
* @param distanceInMeter the distance in meters
* @param metricUnits true to use metric
*/
@VisibleForTesting
static String getDistance(double distanceInMeter, boolean metricUnits) {
double distanceInKilometer = distanceInMeter * UnitConversions.M_TO_KM;
double distance = metricUnits ? distanceInKilometer
: distanceInKilometer * UnitConversions.KM_TO_MI;
return NUMBER_FORMAT.format(distance);
}
/**
* Gets the speed. Performs unit conversion and formatting.
*
* @param speedInMeterPerSecond the speed in meters per second
* @param metricUnits true to use metric
*/
@VisibleForTesting
static String getSpeed(double speedInMeterPerSecond, boolean metricUnits) {
double speedInKilometerPerHour = speedInMeterPerSecond * UnitConversions.MS_TO_KMH;
double speed = metricUnits ? speedInKilometerPerHour
: speedInKilometerPerHour * UnitConversions.KM_TO_MI;
return NUMBER_FORMAT.format(speed);
}
/**
* Gets the elevation. Performs unit conversion and formatting.
*
* @param elevationInMeter the elevation value in meters
* @param metricUnits true to use metric
*/
@VisibleForTesting
static String getElevation(double elevationInMeter, boolean metricUnits) {
double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
return INTEGER_FORMAT.format(elevation);
}
/**
* Adds a row to a Google Spreadsheet worksheet.
*
* @param worksheetUri the worksheet URI
* @param rowContent the row content
* @param authToken the auth token
*/
private static void addRow(String worksheetUri, String rowContent, String authToken)
throws IOException {
URL url = new URL(worksheetUri);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE);
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
conn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
writer.write(rowContent);
writer.flush();
// Get the response
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((reader.readLine()) != null) {
// Just read till the end
}
writer.close();
reader.close();
}
}