Upgrade send to fusion table to Fusion Table api v1.

This commit is contained in:
Jimmy Shih
2013-02-08 15:55:14 -08:00
parent 111cebf0ec
commit ab61869956
11 changed files with 391 additions and 554 deletions
@@ -19,6 +19,7 @@ package com.google.android.apps.mytracks.io.drive;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask; 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.io.sync.SyncUtils;
import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.auth.UserRecoverableAuthException;
@@ -69,8 +70,8 @@ public class SendDriveAsyncTask extends AbstractSendAsyncTask {
@Override @Override
protected boolean performTask() { protected boolean performTask() {
try { try {
GoogleAccountCredential credential = SyncUtils.getGoogleAccountCredential( GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name); context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (credential == null) { if (credential == null) {
return false; return false;
} }
@@ -102,16 +103,18 @@ public class SendDriveAsyncTask extends AbstractSendAsyncTask {
} }
addPermission(drive, id); addPermission(drive, id);
return true; return true;
} catch (UserRecoverableAuthIOException e) {
SyncUtils.sendNotification(context, account.name, e.getIntent());
return false;
} catch (IOException e) {
return retryTask();
} catch (UserRecoverableAuthException e) { } catch (UserRecoverableAuthException e) {
SyncUtils.sendNotification(context, account.name, e.getIntent()); SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false; return false;
} catch (GoogleAuthException e) { } catch (GoogleAuthException e) {
return retryTask(); return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
} }
} }
@@ -11,39 +11,33 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType; 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.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils; import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils; 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.apps.mytracks.util.SystemUtils; import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.GoogleHeaders; import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.MethodOverride; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.GenericUrl; import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.HttpRequest; import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.http.HttpRequestFactory; import com.google.api.services.fusiontables.Fusiontables;
import com.google.api.client.http.HttpResponse; import com.google.api.services.fusiontables.Fusiontables.Query.Sql;
import com.google.api.client.http.HttpTransport; import com.google.api.services.fusiontables.model.Column;
import com.google.api.client.http.InputStreamContent; import com.google.api.services.fusiontables.model.PointStyle;
import com.google.common.annotations.VisibleForTesting; 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.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Vector;
/** /**
* AsyncTask to send a track to Google Fusion Tables. * AsyncTask to send a track to Google Fusion Tables.
@@ -52,20 +46,10 @@ import java.util.Vector;
*/ */
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
@VisibleForTesting private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
public static final String APP_NAME_PREFIX = "Google-MyTracks-";
private static final String SQL_KEY = "sql=";
@VisibleForTesting
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
@VisibleForTesting
public 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 int MAX_POINTS_PER_UPLOAD = 2048;
@VisibleForTesting
public static final String GDATA_VERSION = "2";
private static final int PROGRESS_CREATE_TABLE = 0; private static final int PROGRESS_CREATE_TABLE = 0;
private static final int PROGRESS_UNLIST_TABLE = 5; private static final int PROGRESS_SET_STYLE = 5;
private static final int PROGRESS_UPLOAD_DATA_MIN = 10; private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
private static final int PROGRESS_UPLOAD_DATA_MAX = 90; private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
@@ -78,17 +62,11 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
private static final String MARKER_TYPE_WAYPOINT = "large_blue"; private static final String MARKER_TYPE_WAYPOINT = "large_blue";
private static final String MARKER_TYPE_STATISTICS = "large_yellow"; private static final String MARKER_TYPE_STATISTICS = "large_yellow";
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
private final Context context;
private final long trackId; private final long trackId;
private final Account account; private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils; private final MyTracksProviderUtils myTracksProviderUtils;
private final HttpRequestFactory httpRequestFactory;
// The following variables are for per upload states
private String authToken;
private String tableId;
int currentSegment; int currentSegment;
public SendFusionTablesAsyncTask( public SendFusionTablesAsyncTask(
@@ -96,119 +74,131 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
super(activity); super(activity);
this.trackId = trackId; this.trackId = trackId;
this.account = account; this.account = account;
context = activity.getApplicationContext(); context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
} }
@Override @Override
protected void closeConnection() { protected void closeConnection() {}
// No action needed for Google Fusion Tables
}
@Override @Override
protected void saveResult() { protected void saveResult() {}
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return;
}
track.setTableId(tableId);
myTracksProviderUtils.updateTrack(track);
}
@Override @Override
protected boolean performTask() { protected boolean performTask() {
// Reset the per upload states
authToken = null;
tableId = null;
currentSegment = 1;
try { try {
authToken = AccountManager.get(context) // Reset the per upload states
.blockingGetAuthToken(account, SendFusionTablesUtils.SERVICE, false); currentSegment = 1;
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e); GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
return retryTask(); context, account.name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
} catch (AuthenticatorException e) { if (credential == null) {
Log.d(TAG, "Unable to get auth token", e); 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);
// 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;
}
track.setTableId(tableId);
myTracksProviderUtils.updateTrack(track);
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(); return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask(); return retryTask();
} }
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);
if (!createNewTable(track)) {
// Retry upload in case the auth token is invalid
return retryTask();
}
// 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;
} }
@Override @Override
protected void invalidateToken() { protected void invalidateToken() {}
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/** /**
* Creates a new table. * Creates a new table.
* *
* @param fusiontables fusion tables
* @param track the track * @param track the track
* @return true if success. * @return the table id if success.
*/ */
private boolean createNewTable(Track track) { private String createNewTable(Fusiontables fusiontables, Track track) throws IOException {
String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName()) Table table = new Table();
+ "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; table.setName(track.getName());
return sendQuery(query, true); 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();
} }
/** /**
* Unlists a table. * Uploads all the points in a track. *
*
* @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 fusiontables fusion tables
* @param tableId the table id
* @param track the track * @param track the track
* @return true if success. * @return true if success.
*/ */
private boolean uploadAllTrackPoints(Track track) { private boolean uploadAllTrackPoints(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, -1L, -1, false); cursor = myTracksProviderUtils.getTrackPointCursor(trackId, -1L, -1, false);
@@ -221,15 +211,6 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD); List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null; Location lastLocation = null;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (count / 250.0));
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
TripStatisticsUpdater tripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
int minRecordingDistance = PreferencesUtils.getInt(context,
R.string.min_recording_distance_key, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
cursor.moveToPosition(i); cursor.moveToPosition(i);
@@ -239,17 +220,9 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
if (i == 0) { if (i == 0) {
// Create a start marker // Create a start marker
String name = context.getString(R.string.marker_label_start, track.getName()); String name = context.getString(R.string.marker_label_start, track.getName());
if (!createNewPoint(name, "", location, MARKER_TYPE_START)) { createNewPoint(fusiontables, tableId, name, "", location, MARKER_TYPE_START);
Log.d(TAG, "Unable to create the start marker");
return false;
}
} }
tripStatisticsUpdater.addLocation(location, minRecordingDistance);
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
}
if (LocationUtils.isValidLocation(location)) { if (LocationUtils.isValidLocation(location)) {
lastLocation = location; lastLocation = location;
} }
@@ -257,7 +230,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
// Upload periodically // Upload periodically
int readCount = i + 1; int readCount = i + 1;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) { if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(track, locations, false)) { if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, false)) {
Log.d(TAG, "Unable to upload points"); Log.d(TAG, "Unable to upload points");
return false; return false;
} }
@@ -267,25 +240,18 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
} }
// Do a final upload with the remaining locations // Do a final upload with the remaining locations
if (!prepareAndUploadPoints(track, locations, true)) { if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, true)) {
Log.d(TAG, "Unable to upload points"); Log.d(TAG, "Unable to upload points");
return false; return false;
} }
// Create an end marker // Create an end marker
if (lastLocation != null) { if (lastLocation != null) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription(
descriptionGenerator.generateTrackDescription(track, distances, elevations, true));
String name = context.getString(R.string.marker_label_end, track.getName()); String name = context.getString(R.string.marker_label_end, track.getName());
if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) { DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
Log.d(TAG, "Unable to create the end marker"); String description = descriptionGenerator.generateTrackDescription(track, null, null, true);
return false; createNewPoint(fusiontables, tableId, name, description, lastLocation, MARKER_TYPE_END);
}
} }
return true; return true;
} finally { } finally {
if (cursor != null) { if (cursor != null) {
@@ -297,11 +263,14 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
/** /**
* Prepares and uploads a list of locations from a track. * Prepares and uploads a list of locations from a track.
* *
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track * @param track the track
* @param locations the locations from the track * @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations * @param lastBatch true if it is the last batch of locations
*/ */
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) { private boolean prepareAndUploadPoints(Fusiontables fusiontables, String tableId, Track track,
List<Location> locations, boolean lastBatch) throws IOException {
// Prepare locations // Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations); ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
@@ -312,10 +281,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
splitTrack.setName(context.getString( splitTrack.setName(context.getString(
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment)); R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
} }
if (!createNewLineString(splitTrack)) { createNewLineString(fusiontables, tableId, splitTrack);
Log.d(TAG, "Upload points failed");
return false;
}
currentSegment++; currentSegment++;
} }
return true; return true;
@@ -324,24 +290,28 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
/** /**
* Uploads all the waypoints. * Uploads all the waypoints.
* *
* @param fusiontables fusion tables
* @param tableId the table id
* @return true if success. * @return true if success.
* @throws IOException
*/ */
private boolean uploadWaypoints() { private boolean uploadWaypoints(Fusiontables fusiontables, String tableId) throws IOException {
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = myTracksProviderUtils.getWaypointCursor( cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS); trackId, -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the /*
// track). * This will skip the first waypoint (it carries the stats for the
* track).
*/
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor); Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
String type = wpt.getType() == WaypointType.STATISTICS ? MARKER_TYPE_STATISTICS String type = wpt.getType() == WaypointType.STATISTICS ? MARKER_TYPE_STATISTICS
: MARKER_TYPE_WAYPOINT; : MARKER_TYPE_WAYPOINT;
if (!createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), type)) { String description = wpt.getDescription().replaceAll("\n", "<br>");
Log.d(TAG, "Upload waypoints failed"); createNewPoint(
return false; fusiontables, tableId, wpt.getName(), description, wpt.getLocation(), type);
}
} }
} }
return true; return true;
@@ -355,110 +325,37 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
/** /**
* Creates a new row in Google Fusion Tables representing a marker as a point. * 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 name the marker name
* @param description the marker description * @param description the marker description
* @param location the marker location * @param location the marker location
* @param type the marker type * @param type the marker type
* @return true if success.
*/ */
private boolean createNewPoint(String name, String description, Location location, String type) { private void createNewPoint(Fusiontables fusiontables, String tableId, String name,
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " String description, Location location, String type) throws IOException {
+ SendFusionTablesUtils.formatSqlValues( String values = SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type); name, description, SendFusionTablesUtils.getKmlPoint(location), type);
return sendQuery(query, false); 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 * Creates a new row in Google Fusion Tables representing the track as a line
* segment. * segment.
* *
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track * @param track the track
* @return true if success.
*/ */
private boolean createNewLineString(Track track) { private void createNewLineString(Fusiontables fusiontables, String tableId, Track track)
String query = "INSERT INTO " + tableId throws IOException {
+ " (name,description,geometry) VALUES " + SendFusionTablesUtils.formatSqlValues( String values = SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
track.getName(), track.getDescription(), SendFusionTablesUtils.getKmlLineString(track.getLocations()));
SendFusionTablesUtils.getKmlLineString(track.getLocations())); Sql sql = fusiontables.query()
return sendQuery(query, false); .sql("INSERT INTO " + tableId + " (name,description,geometry) VALUES " + values);
} sql.execute();
/**
* 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;
try {
sql = SQL_KEY + URLEncoder.encode(query, "UTF-8");
} catch (UnsupportedEncodingException e1) {
Log.d(TAG, "Unable to encode query", e1);
return false;
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(sql.getBytes());
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
HttpRequest request;
try {
request = httpRequestFactory.buildPostRequest(url, inputStreamContent);
} catch (IOException e) {
Log.d(TAG, "Unable to build request", e);
return false;
}
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context));
headers.setGDataVersion(GDATA_VERSION);
headers.setGoogleLogin(authToken);
headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers);
HttpResponse response;
try {
response = request.execute();
} catch (IOException e) {
Log.d(TAG, "Unable to execute request", e);
return false;
}
boolean isSuccess = response.isSuccessStatusCode();
if (isSuccess) {
InputStream content = null;
try {
content = response.getContent();
if (setTableId) {
tableId = SendFusionTablesUtils.getTableId(content);
if (tableId == null) {
Log.d(TAG, "tableId is null");
return false;
}
}
} catch (IOException e) {
Log.d(TAG, "Unable to get response", e);
return false;
} finally {
if (content != null) {
try {
content.close();
} catch (IOException e) {
Log.d(TAG, "Unable to close content", e);
}
}
}
} else {
Log.d(TAG,
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
return false;
}
return true;
} }
/** /**
@@ -22,9 +22,6 @@ import com.google.common.annotations.VisibleForTesting;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
@@ -107,7 +104,8 @@ public class SendFusionTablesUtils {
* @param string the string * @param string the string
* @return the escaped string. * @return the escaped string.
*/ */
public static String escapeSqlString(String string) { @VisibleForTesting
static String escapeSqlString(String string) {
return string.replaceAll("'", "''"); return string.replaceAll("'", "''");
} }
@@ -160,45 +158,4 @@ public class SendFusionTablesUtils {
builder.append(location.getAltitude()); 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, "Unable to read result", e);
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, "Unable to parse result", e);
return null;
}
String[] lines = s.split("\n");
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;
}
}
} }
@@ -20,13 +20,11 @@ import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity; import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.drive.SendDriveActivity; import com.google.android.apps.mytracks.io.drive.SendDriveActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity; import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient; import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient; import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants; import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.ChooseMapActivity; import com.google.android.apps.mytracks.io.maps.ChooseMapActivity;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity; import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.IntentUtils; import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -76,16 +74,16 @@ public class AccountChooserActivity extends Activity {
if (accounts.length == 1) { if (accounts.length == 1) {
sendRequest.setAccount(accounts[0]); sendRequest.setAccount(accounts[0]);
PreferencesUtils.setString(this, R.string.google_account_key, accounts[0].name); PreferencesUtils.setString(this, R.string.google_account_key, accounts[0].name);
checkDrivePermission(accounts[0].name); checkDrivePermission();
return; return;
} }
String googleAccount = PreferencesUtils.getString(this, R.string.google_account_key, String googleAccount = PreferencesUtils.getString(
PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT); this, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
for (int i = 0; i < accounts.length; i++) { for (int i = 0; i < accounts.length; i++) {
if (accounts[i].name.equals(googleAccount)) { if (accounts[i].name.equals(googleAccount)) {
sendRequest.setAccount(accounts[i]); sendRequest.setAccount(accounts[i]);
checkDrivePermission(accounts[i].name); checkDrivePermission();
return; return;
} }
} }
@@ -95,14 +93,22 @@ public class AccountChooserActivity extends Activity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
case SyncUtils.DRIVE_PERMISSION_REQUEST_CODE: case SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE:
SyncUtils.cancelNotification(this); SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
driveCallback.onSuccess(); driveCallback.onSuccess();
} else { } else {
driveCallback.onFailure(); driveCallback.onFailure();
} }
break; break;
case SendToGoogleUtils.FUSION_TABLES_PERMISSION_REQUEST_CODE:
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) {
fusionTablesCallback.onSuccess();
} else {
fusionTablesCallback.onFailure();
}
break;
default: default:
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
} }
@@ -112,22 +118,18 @@ public class AccountChooserActivity extends Activity {
protected Dialog onCreateDialog(int id) { protected Dialog onCreateDialog(int id) {
switch (id) { switch (id) {
case DIALOG_NO_ACCOUNT_ID: case DIALOG_NO_ACCOUNT_ID:
return new AlertDialog.Builder(this) return new AlertDialog.Builder(this).setCancelable(true)
.setCancelable(true)
.setMessage(R.string.send_google_no_account_message) .setMessage(R.string.send_google_no_account_message)
.setOnCancelListener(new DialogInterface.OnCancelListener() { .setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
finish(); finish();
} }
}) }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
finish(); finish();
} }
}) }).setTitle(R.string.send_google_no_account_title).create();
.setTitle(R.string.send_google_no_account_title)
.create();
case DIALOG_CHOOSER_ID: case DIALOG_CHOOSER_ID:
return createChooserDialog(); return createChooserDialog();
default: default:
@@ -143,98 +145,110 @@ public class AccountChooserActivity extends Activity {
for (int i = 0; i < accounts.length; i++) { for (int i = 0; i < accounts.length; i++) {
choices[i] = accounts[i].name; choices[i] = accounts[i].name;
} }
return new AlertDialog.Builder(this) return new AlertDialog.Builder(this).setCancelable(true)
.setCancelable(true)
.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() { .setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
finish(); finish();
} }
}) }).setOnCancelListener(new DialogInterface.OnCancelListener() {
.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override
@Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
finish(); finish();
} }
}) }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Account account = accounts[position]; Account account = accounts[position];
PreferencesUtils.setString( PreferencesUtils.setString(
AccountChooserActivity.this, R.string.google_account_key, account.name); AccountChooserActivity.this, R.string.google_account_key, account.name);
sendRequest.setAccount(account); sendRequest.setAccount(account);
checkDrivePermission(account.name); checkDrivePermission();
} }
}) }).setSingleChoiceItems(choices, 0, null)
.setSingleChoiceItems(choices, 0, null) .setTitle(R.string.send_google_choose_account_title).create();
.setTitle(R.string.send_google_choose_account_title)
.create();
} }
/** /**
* Checks the Drive permission. * Checks the Drive permission.
*
* @param accountName the account name
*/ */
private void checkDrivePermission(String accountName) { private void checkDrivePermission() {
if (sendRequest.isSendDrive()) { if (sendRequest.isSendDrive()) {
SyncUtils.checkPermissionByActivity(this, accountName, driveCallback); SendToGoogleUtils.checkPermissionByActivity(this, sendRequest.getAccount().name,
SendToGoogleUtils.DRIVE_SCOPE, SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE,
driveCallback);
} else { } else {
driveCallback.onSuccess(); driveCallback.onSuccess();
} }
} }
/**
* Checks the Fusion Tables permission.
*/
private void checkFusionTablesPermission() {
if (sendRequest.isSendFusionTables()) {
SendToGoogleUtils.checkPermissionByActivity(this, sendRequest.getAccount().name,
SendToGoogleUtils.FUSION_TABLES_SCOPE,
SendToGoogleUtils.FUSION_TABLES_PERMISSION_REQUEST_CODE, fusionTablesCallback);
} else {
fusionTablesCallback.onSuccess();
}
}
private PermissionCallback spreadsheetsCallback = new PermissionCallback() { private PermissionCallback spreadsheetsCallback = new PermissionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
startNextActivity(); startNextActivity();
} }
@Override
@Override
public void onFailure() { public void onFailure() {
handleNoAccountPermission(); handleNoAccountPermission();
} }
}; };
private PermissionCallback docsCallback = new PermissionCallback() { private PermissionCallback docsCallback = new PermissionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
getPermission(SpreadsheetsClient.SERVICE, sendRequest.isSendDocs(), spreadsheetsCallback); getPermission(SpreadsheetsClient.SERVICE, sendRequest.isSendDocs(), spreadsheetsCallback);
} }
@Override
@Override
public void onFailure() { public void onFailure() {
handleNoAccountPermission(); handleNoAccountPermission();
} }
}; };
private PermissionCallback fusionTablesCallback = new PermissionCallback() { private PermissionCallback fusionTablesCallback = new PermissionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
getPermission(DocumentsClient.SERVICE, sendRequest.isSendDocs(), docsCallback); getPermission(DocumentsClient.SERVICE, sendRequest.isSendDocs(), docsCallback);
} }
@Override
@Override
public void onFailure() { public void onFailure() {
handleNoAccountPermission(); handleNoAccountPermission();
} }
}; };
private PermissionCallback mapsCallback = new PermissionCallback() { private PermissionCallback mapsCallback = new PermissionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
getPermission( checkFusionTablesPermission();
SendFusionTablesUtils.SERVICE, sendRequest.isSendFusionTables(), fusionTablesCallback);
} }
@Override
@Override
public void onFailure() { public void onFailure() {
handleNoAccountPermission(); handleNoAccountPermission();
} }
}; };
private PermissionCallback driveCallback = new PermissionCallback() { private PermissionCallback driveCallback = new PermissionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback); getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback);
} }
@Override
@Override
public void onFailure() { public void onFailure() {
handleNoAccountPermission(); handleNoAccountPermission();
} }
@@ -252,7 +266,7 @@ public class AccountChooserActivity extends Activity {
if (needPermission) { if (needPermission) {
AccountManager.get(this).getAuthToken(sendRequest.getAccount(), authTokenType, null, this, AccountManager.get(this).getAuthToken(sendRequest.getAccount(), authTokenType, null, this,
new AccountManagerCallback<Bundle>() { new AccountManagerCallback<Bundle>() {
@Override @Override
public void run(AccountManagerFuture<Bundle> future) { public void run(AccountManagerFuture<Bundle> future) {
try { try {
if (future.getResult().getString(AccountManager.KEY_AUTHTOKEN) != null) { if (future.getResult().getString(AccountManager.KEY_AUTHTOKEN) != null) {
@@ -290,7 +304,6 @@ public class AccountChooserActivity extends Activity {
* !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity} * !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity}
* <p> * <p>
* !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity} * !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity}
*
*/ */
private void startNextActivity() { private void startNextActivity() {
Class<?> next; Class<?> next;
@@ -19,11 +19,25 @@ package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
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.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.fusiontables.FusiontablesScopes;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location; import android.location.Location;
import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -36,6 +50,16 @@ public class SendToGoogleUtils {
private static final String TAG = SendToGoogleUtils.class.getSimpleName(); private static final String TAG = SendToGoogleUtils.class.getSimpleName();
// Request code to obtain Drive permission
public static final int DRIVE_PERMISSION_REQUEST_CODE = 1;
public static final String DRIVE_SCOPE = DriveScopes.DRIVE;
public static final int DRIVE_NOTIFICATION_ID = 1;
// Request code to obtain Fusion Tables permission
public static final int FUSION_TABLES_PERMISSION_REQUEST_CODE = 2;
public static final String FUSION_TABLES_SCOPE = FusiontablesScopes.FUSIONTABLES;
public static final int FUSION_TABLES_NOTIFICATION_ID = 2;
private SendToGoogleUtils() {} private SendToGoogleUtils() {}
/** /**
@@ -115,4 +139,105 @@ public class SendToGoogleUtils {
splitTracks.add(segment); splitTracks.add(segment);
return true; return true;
} }
/**
* Gets the google account credential.
*
* @param context the context
* @param accountName the account name
* @param scope the scope
*/
public static GoogleAccountCredential getGoogleAccountCredential(
Context context, String accountName, String scope) throws IOException, GoogleAuthException {
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context, scope);
credential.setSelectedAccountName(accountName);
credential.getToken();
return credential;
}
/**
* Checks permission by an activity. Will start an activity to request
* permission using the request code.
*
* @param activity the activity
* @param accountName the account name
* @param scope the scope
* @param requestCode the request code
* @param permissionCallback the permission callback
*/
public static void checkPermissionByActivity(final Activity activity, final String accountName,
final String scope, final int requestCode, final PermissionCallback permissionCallback) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
getGoogleAccountCredential(activity, accountName, scope);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onSuccess();
}
});
} catch (UserRecoverableAuthException e) {
activity.startActivityForResult(e.getIntent(), requestCode);
} catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onFailure();
}
});
} catch (UserRecoverableAuthIOException e) {
activity.startActivityForResult(e.getIntent(), requestCode);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onFailure();
}
});
}
}
});
thread.start();
}
/**
* Sends a notification to request permission.
*
* @param context the context
* @param accountName the account name
* @param intent the intent
* @param notificaitonId the notification id
*/
public static void sendNotification(
Context context, String accountName, Intent intent, int notificaitonId) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).addFlags(Intent.FLAG_FROM_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setAutoCancel(true)
.setContentIntent(pendingIntent)
.setContentText(context.getString(R.string.permission_request_message, accountName))
.setContentTitle(context.getString(R.string.permission_request_title))
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setTicker(context.getString(R.string.permission_request_title));
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(notificaitonId, builder.build());
}
/**
* Cancels any notification to request a permission.
*
* @param context the context
* @param notificationId the notification id
*/
public static void cancelNotification(Context context, int notificationId) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
} }
@@ -19,6 +19,7 @@ package com.google.android.apps.mytracks.io.sync;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.file.KmlImporter; import com.google.android.apps.mytracks.io.file.KmlImporter;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.auth.UserRecoverableAuthException;
@@ -101,8 +102,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
} }
try { try {
GoogleAccountCredential credential = SyncUtils.getGoogleAccountCredential( GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name); context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (credential == null) { if (credential == null) {
return; return;
} }
@@ -125,11 +126,13 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
} }
insertNewTracks(folderId); insertNewTracks(folderId);
} catch (UserRecoverableAuthException e) { } catch (UserRecoverableAuthException e) {
SyncUtils.sendNotification(context, account.name, e.getIntent()); SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
} catch (GoogleAuthException e) { } catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e); Log.e(TAG, "GoogleAuthException", e);
} catch (UserRecoverableAuthIOException e) { } catch (UserRecoverableAuthIOException e) {
SyncUtils.sendNotification(context, account.name, e.getIntent()); SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "IOException", e); Log.e(TAG, "IOException", e);
} }
@@ -22,11 +22,8 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackFileFormat; import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.TrackWriter; import com.google.android.apps.mytracks.io.file.TrackWriter;
import com.google.android.apps.mytracks.io.sendtogoogle.PermissionCallback;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; 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.android.maps.mytracks.R;
import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
@@ -35,7 +32,6 @@ import com.google.api.client.http.FileContent;
import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive; import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Files.List; import com.google.api.services.drive.Drive.Files.List;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.File; import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList; import com.google.api.services.drive.model.FileList;
import com.google.api.services.drive.model.ParentReference; import com.google.api.services.drive.model.ParentReference;
@@ -43,14 +39,9 @@ import com.google.common.annotations.VisibleForTesting;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import java.io.FileInputStream; import java.io.FileInputStream;
@@ -70,9 +61,6 @@ import java.util.Locale;
*/ */
public class SyncUtils { public class SyncUtils {
// Request code to obtain Drive permission
public static final int DRIVE_PERMISSION_REQUEST_CODE = 1;
// Get tracks with drive id // Get tracks with drive id
public static final String DRIVE_ID_TRACKS_QUERY = TracksColumns.DRIVEID + " IS NOT NULL AND " public static final String DRIVE_ID_TRACKS_QUERY = TracksColumns.DRIVEID + " IS NOT NULL AND "
+ TracksColumns.DRIVEID + "!=''"; + TracksColumns.DRIVEID + "!=''";
@@ -103,106 +91,9 @@ public class SyncUtils {
private static final String TAG = SyncUtils.class.getSimpleName(); private static final String TAG = SyncUtils.class.getSimpleName();
private static final String SYNC_AUTHORITY = "com.google.android.maps.mytracks"; private static final String SYNC_AUTHORITY = "com.google.android.maps.mytracks";
private static final int NOTIFICATION_ID = 0;
private SyncUtils() {} private SyncUtils() {}
/**
* Gets the google account credential.
*
* @param context the context
* @param accountName the account name
*/
public static GoogleAccountCredential getGoogleAccountCredential(
Context context, String accountName) throws IOException, GoogleAuthException {
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(
context, DriveScopes.DRIVE);
credential.setSelectedAccountName(accountName);
credential.getToken();
return credential;
}
/**
* Checks permission by an activity. Will start an activity to request
* permission with request code {@link #DRIVE_PERMISSION_REQUEST_CODE}.
*
* @param activity the activity
* @param accountName the account name
* @param permissionCallback the permission callback
*/
public static void checkPermissionByActivity(final Activity activity, final String accountName,
final PermissionCallback permissionCallback) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
getGoogleAccountCredential(activity, accountName);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onSuccess();
}
});
} catch (UserRecoverableAuthException e) {
activity.startActivityForResult(e.getIntent(), DRIVE_PERMISSION_REQUEST_CODE);
} catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onFailure();
}
});
} catch (UserRecoverableAuthIOException e) {
activity.startActivityForResult(e.getIntent(), DRIVE_PERMISSION_REQUEST_CODE);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
permissionCallback.onFailure();
}
});
}
}
});
thread.start();
}
/**
* Sends a notification to request permission.
*
* @param context the context
* @param accountName the account name
* @param intent the intent
*/
public static void sendNotification(Context context, String accountName, Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).addFlags(Intent.FLAG_FROM_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setAutoCancel(true)
.setContentIntent(pendingIntent)
.setContentText(context.getString(R.string.permission_request_message, accountName))
.setContentTitle(context.getString(R.string.permission_request_title))
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setTicker(context.getString(R.string.permission_request_title));
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
/**
* Cancels any notification to request drive permission.
*
* @param context the context
*/
public static void cancelNotification(Context context) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFICATION_ID);
}
/** /**
* Syncs now for the current account. * Syncs now for the current account.
* *
@@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sendtogoogle.PermissionCallback; import com.google.android.apps.mytracks.io.sendtogoogle.PermissionCallback;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils; import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.DialogUtils; import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -143,8 +144,8 @@ public class GoogleSettingsActivity extends AbstractSettingsActivity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
case SyncUtils.DRIVE_PERMISSION_REQUEST_CODE: case SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE:
SyncUtils.cancelNotification(this); SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
permissionCallback.onSuccess(); permissionCallback.onSuccess();
} else { } else {
@@ -173,8 +174,9 @@ public class GoogleSettingsActivity extends AbstractSettingsActivity {
String googleAccount = PreferencesUtils.getString( String googleAccount = PreferencesUtils.getString(
GoogleSettingsActivity.this, R.string.google_account_key, GoogleSettingsActivity.this, R.string.google_account_key,
PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT); PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
SyncUtils.checkPermissionByActivity( SendToGoogleUtils.checkPermissionByActivity(GoogleSettingsActivity.this,
GoogleSettingsActivity.this, googleAccount, permissionCallback); googleAccount, SendToGoogleUtils.DRIVE_SCOPE,
SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE, permissionCallback);
} }
}); });
break; break;
@@ -20,8 +20,6 @@ import com.google.android.apps.mytracks.stats.TripStatistics;
import android.location.Location; import android.location.Location;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import junit.framework.TestCase; import junit.framework.TestCase;
@@ -225,62 +223,4 @@ public class SendFusionTablesUtilsTest extends TestCase {
SendFusionTablesUtils.appendLocation(location, builder); SendFusionTablesUtils.appendLocation(location, builder);
assertEquals("10.1,20.2,30.3", builder.toString()); 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));
}
} }
@@ -17,7 +17,6 @@ package com.google.android.apps.mytracks.endtoendtest;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.docs.SendDocsUtils; import com.google.android.apps.mytracks.io.docs.SendDocsUtils;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesAsyncTask;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils; import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient; import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
@@ -63,11 +62,17 @@ import java.util.Locale;
* @author Youtao Liu * @author Youtao Liu
*/ */
public class GoogleUtils { public class GoogleUtils {
public static final String DOCUMENT_NAME_PREFIX = "My Tracks"; public static final String DOCUMENT_NAME_PREFIX = "My Tracks";
public static final String ACCOUNT_NAME_1 = "mytrackstest@gmail.com"; public static final String ACCOUNT_NAME_1 = "mytrackstest@gmail.com";
public static final String ACCOUNT_NAME_2 = "mytrackstest2@gmail.com"; public static final String ACCOUNT_NAME_2 = "mytrackstest2@gmail.com";
public static final String SPREADSHEET_NAME = DOCUMENT_NAME_PREFIX + "-" + EndToEndTestUtils.activityType; public static final String SPREADSHEET_NAME = DOCUMENT_NAME_PREFIX + "-" + EndToEndTestUtils.activityType;
private static final String APP_NAME_PREFIX = "Google-MyTracks-";
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
private static final String FUSION_TABLES_BASE_URL = "https://www.google.com/fusiontables/api/query";
private static final String GDATA_VERSION = "2";
/** /**
* Gets the account to access Google Services. * Gets the account to access Google Services.
* *
@@ -408,7 +413,7 @@ public class GoogleUtils {
String fusionTableAuthToken = AccountManager.get(context).blockingGetAuthToken(getAccount(context), String fusionTableAuthToken = AccountManager.get(context).blockingGetAuthToken(getAccount(context),
SendFusionTablesUtils.SERVICE, false); SendFusionTablesUtils.SERVICE, false);
GenericUrl url = new GenericUrl(SendFusionTablesAsyncTask.FUSION_TABLES_BASE_URL); GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL);
String sql = "sql=" + query; String sql = "sql=" + query;
ByteArrayInputStream inputStream = new ByteArrayInputStream(sql.getBytes()); ByteArrayInputStream inputStream = new ByteArrayInputStream(sql.getBytes());
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream); InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
@@ -418,11 +423,11 @@ public class GoogleUtils {
.createRequestFactory(new MethodOverride())).buildPostRequest(url, inputStreamContent); .createRequestFactory(new MethodOverride())).buildPostRequest(url, inputStreamContent);
GoogleHeaders headers = new GoogleHeaders(); GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(SendFusionTablesAsyncTask.APP_NAME_PREFIX headers.setApplicationName(APP_NAME_PREFIX
+ SystemUtils.getMyTracksVersion(context)); + SystemUtils.getMyTracksVersion(context));
headers.setGDataVersion(SendFusionTablesAsyncTask.GDATA_VERSION); headers.setGDataVersion(GDATA_VERSION);
headers.setGoogleLogin(fusionTableAuthToken); headers.setGoogleLogin(fusionTableAuthToken);
headers.setContentType(SendFusionTablesAsyncTask.CONTENT_TYPE); headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers); request.setHeaders(headers);
HttpResponse response; HttpResponse response;
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.endtoendtest.sync;
import com.google.android.apps.mytracks.TrackListActivity; import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.endtoendtest.EndToEndTestUtils; import com.google.android.apps.mytracks.endtoendtest.EndToEndTestUtils;
import com.google.android.apps.mytracks.endtoendtest.GoogleUtils; import com.google.android.apps.mytracks.endtoendtest.GoogleUtils;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils; import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthException;
@@ -119,8 +120,8 @@ public class SyncTestUtils {
public static Drive getGoogleDrive(Context context) throws IOException, GoogleAuthException { public static Drive getGoogleDrive(Context context) throws IOException, GoogleAuthException {
String googleAccount = PreferencesUtils.getString( String googleAccount = PreferencesUtils.getString(
context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT); context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
GoogleAccountCredential credential = SyncUtils.getGoogleAccountCredential( GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, googleAccount); context, googleAccount, SendToGoogleUtils.DRIVE_SCOPE);
return SyncUtils.getDriveService(credential); return SyncUtils.getDriveService(credential);
} }