// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.GoogleUrl;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.HttpContent;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.Permission;
import com.google.api.services.fusiontables.Fusiontables;
import com.google.api.services.fusiontables.Fusiontables.Query.Sql;
import com.google.api.services.fusiontables.model.Column;
import com.google.api.services.fusiontables.model.PointStyle;
import com.google.api.services.fusiontables.model.StyleFunction;
import com.google.api.services.fusiontables.model.StyleSetting;
import com.google.api.services.fusiontables.model.Table;
import com.google.api.services.fusiontables.model.Template;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* AsyncTask to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final int PROGRESS_CREATE_TABLE = 0;
private static final int PROGRESS_SET_STYLE = 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_blue";
private static final String MARKER_TYPE_STATISTICS = "large_yellow";
private final long trackId;
private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
int currentSegment;
public SendFusionTablesAsyncTask(
SendFusionTablesActivity activity, long trackId, Account account) {
super(activity);
this.trackId = trackId;
this.account = account;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
// Reset the per upload states
currentSegment = 1;
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
if (credential == null) {
return false;
}
Fusiontables fusiontables = new Fusiontables.Builder(
AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return false;
}
// Create a new table
publishProgress(PROGRESS_CREATE_TABLE);
String tableId = createNewTable(fusiontables, track);
if (tableId == null) {
return retryTask();
}
publishProgress(PROGRESS_SET_STYLE);
setStyle(fusiontables, tableId);
setTemplate(fusiontables, tableId);
if (!setPermission(track, tableId)) {
Log.d(TAG, "Cannot set permission for table " + tableId);
return false;
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(fusiontables, tableId, track)) {
return false;
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints(fusiontables, tableId)) {
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
}
}
@Override
protected void invalidateToken() {}
/**
* Creates a new table.
*
* @param fusiontables fusion tables
* @param track the track
* @return the table id if success.
*/
private String createNewTable(Fusiontables fusiontables, Track track) throws IOException {
Table table = new Table();
table.setName(track.getName());
table.setDescription(track.getDescription());
table.setIsExportable(true);
table.setColumns(Arrays.asList(new Column().setName("name").setType("STRING"),
new Column().setName("description").setType("STRING"),
new Column().setName("geometry").setType("LOCATION"),
new Column().setName("icon").setType("STRING")));
return fusiontables.table().insert(table).execute().getTableId();
}
private void setStyle(Fusiontables fusiontables, String tableId) throws IOException {
StyleFunction styleFunction = new StyleFunction();
styleFunction.setColumnName("icon");
PointStyle pointStyle = new PointStyle();
pointStyle.setIconStyler(styleFunction);
StyleSetting styleSetting = new StyleSetting();
styleSetting.setTableId(tableId);
styleSetting.setMarkerOptions(pointStyle);
fusiontables.style().insert(tableId, styleSetting).execute();
}
private void setTemplate(Fusiontables fusiontables, String tableId) throws IOException {
Template template = new Template();
template.setTableId(tableId);
template.setAutomaticColumnNames(Arrays.asList("name", "description"));
fusiontables.template().insert(tableId, template).execute();
}
private boolean setPermission(Track track, String tableId) throws IOException, GoogleAuthException {
boolean defaultTablePublic = PreferencesUtils.getBoolean(context,
R.string.export_google_fusion_tables_public_key,
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
if (!defaultTablePublic) {
return true;
}
GoogleAccountCredential driveCredential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (driveCredential == null) {
return false;
}
Drive drive = SyncUtils.getDriveService(driveCredential);
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("anyone");
permission.setValue("");
drive.permissions().insert(tableId, permission).execute();
shareUrl = SendFusionTablesUtils.getMapUrl(track, tableId);
return true;
}
/**
* Uploads all the points in a track. *
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
* @return true if success.
*/
private boolean uploadAllTrackPoints(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
int numberOfPoints = track.getNumberOfPoints();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastValidLocation = null;
boolean sentStartMarker = false;
int readCount = 0;
LocationIterator locationIterator = null;
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(
trackId, -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
locations.add(location);
if (LocationUtils.isValidLocation(location)) {
lastValidLocation = location;
}
if (!sentStartMarker && lastValidLocation != null) {
// Create a start marker
String name = context.getString(R.string.marker_label_start, track.getName());
createNewPoint(fusiontables, tableId, name, "", lastValidLocation, MARKER_TYPE_START);
sentStartMarker = true;
}
// Upload periodically
readCount++;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, numberOfPoints);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastValidLocation != null) {
String name = context.getString(R.string.marker_label_end, track.getName());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
String description = descriptionGenerator.generateTrackDescription(track, null, null, true);
createNewPoint(
fusiontables, tableId, name, description, lastValidLocation, MARKER_TYPE_END);
}
return true;
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @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(Fusiontables fusiontables, String tableId, Track track,
List<Location> locations, boolean lastBatch) throws IOException {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track splitTrack : splitTracks) {
if (!onlyOneSegment) {
splitTrack.setName(context.getString(
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
}
createNewLineString(fusiontables, tableId, splitTrack);
currentSegment++;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @return true if success.
* @throws IOException
*/
private boolean uploadWaypoints(Fusiontables fusiontables, String tableId) throws IOException {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, 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);
String type = wpt.getType() == WaypointType.STATISTICS ? MARKER_TYPE_STATISTICS
: MARKER_TYPE_WAYPOINT;
String description = wpt.getDescription().replaceAll("\n", "<br>");
createNewPoint(
fusiontables, tableId, wpt.getName(), description, wpt.getLocation(), type);
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Creates a new row in Google Fusion Tables representing a marker as a point.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @param type the marker type
*/
private void createNewPoint(Fusiontables fusiontables, String tableId, String name,
String description, Location location, String type) throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
Sql sql = fusiontables.query()
.sql("INSERT INTO " + tableId + " (name,description,geometry,icon) VALUES " + values);
sql.execute();
}
/**
* Creates a new row in Google Fusion Tables representing the track as a line
* segment.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
*/
private void createNewLineString(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
String sql = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + values;
HttpContent content = ByteArrayContent.fromString(null, "sql=" + sql);
GoogleUrl url = new GoogleUrl("https://www.googleapis.com/fusiontables/v1/query");
fusiontables.getRequestFactory().buildPostRequest(url, content).execute();
}
/**
* 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 = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}