Add retry for SyncAdapter.

This commit is contained in:
Jimmy Shih
2013-02-05 17:50:35 -08:00
parent 49d055ce36
commit 08bcfde5c7
6 changed files with 324 additions and 244 deletions
@@ -84,8 +84,8 @@ public class SendDriveAsyncTask extends AbstractSendAsyncTask {
String driveId = track.getDriveId(); String driveId = track.getDriveId();
if (driveId != null && !driveId.equals("")) { if (driveId != null && !driveId.equals("")) {
File driveFile = drive.files().get(driveId).execute(); File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isValid(driveFile, folderId) if (SyncUtils.isValid(driveFile, folderId) && SyncUtils.updateDriveFile(
&& SyncUtils.updateDriveFile(context, myTracksProviderUtils, drive, driveFile, track)) { drive, driveFile, context, myTracksProviderUtils, track, false)) {
addPermission(drive, driveId); addPermission(drive, driveId);
return true; return true;
} }
@@ -95,7 +95,8 @@ public class SendDriveAsyncTask extends AbstractSendAsyncTask {
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
} }
String id = SyncUtils.addDriveFile(context, myTracksProviderUtils, drive, folderId, track); String id = SyncUtils.insertDriveFile(
drive, folderId, context, myTracksProviderUtils, track, false);
if (id == null) { if (id == null) {
return false; return false;
} }
@@ -45,6 +45,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParserFactory;
@@ -174,7 +175,8 @@ abstract class AbstractImporter extends DefaultHandler {
* @param inputStream the input stream. * @param inputStream the input stream.
* @return an array of imported track ids. * @return an array of imported track ids.
*/ */
public long[] importFile(InputStream inputStream) throws Exception { public long[] importFile(InputStream inputStream)
throws SAXException, IOException, ParserConfigurationException {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
SAXParser saxParser = saxParserFactory.newSAXParser(); SAXParser saxParser = saxParserFactory.newSAXParser();
@@ -186,9 +188,11 @@ abstract class AbstractImporter extends DefaultHandler {
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms"); Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
} catch (SAXException e) { } catch (SAXException e) {
handleException(e); handleException();
throw e;
} catch (IOException e) { } catch (IOException e) {
handleException(e); handleException();
throw e;
} }
long[] result = new long[trackIds.size()]; long[] result = new long[trackIds.size()];
for (int i = 0; i < result.length; i++) { for (int i = 0; i < result.length; i++) {
@@ -578,13 +582,10 @@ abstract class AbstractImporter extends DefaultHandler {
/** /**
* Handles an exception. * Handles an exception.
*
* @param e the exception
*/ */
private void handleException(Exception e) throws Exception { private void handleException() {
for (long trackId : trackIds) { for (long trackId : trackIds) {
myTracksProviderUtils.deleteTrack(trackId); myTracksProviderUtils.deleteTrack(trackId);
} }
throw e;
} }
} }
@@ -20,8 +20,11 @@ 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.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.googleapis.extensions.android.gms.auth.GoogleAccountCredential; 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.GenericUrl; import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponse;
import com.google.api.services.drive.Drive; import com.google.api.services.drive.Drive;
@@ -40,7 +43,6 @@ import android.content.Context;
import android.content.SyncResult; import android.content.SyncResult;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@@ -51,6 +53,10 @@ import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/** /**
* SyncAdapter to sync tracks with Google Drive. * SyncAdapter to sync tracks with Google Drive.
* *
@@ -94,17 +100,18 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
GoogleAccountCredential credential = SyncUtils.getCredential(context, account.name);
if (credential == null) {
return;
}
if (drive == null || !driveAccountName.equals(account.name)) {
drive = SyncUtils.getDriveService(credential);
driveAccountName = account.name;
}
try { try {
GoogleAccountCredential credential = SyncUtils.getGoogleAccountCredential(
context, account.name);
if (credential == null) {
return;
}
if (drive == null || !driveAccountName.equals(account.name)) {
drive = SyncUtils.getDriveService(credential);
driveAccountName = account.name;
}
String folderId = SyncUtils.getMyTracksFolder(context, drive); String folderId = SyncUtils.getMyTracksFolder(context, drive);
if (folderId == null) { if (folderId == null) {
return; return;
@@ -117,8 +124,14 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
performIncrementalSync(folderId, largestChangeId); performIncrementalSync(folderId, largestChangeId);
} }
insertNewTracks(folderId); insertNewTracks(folderId);
} catch (Exception e) { } catch (UserRecoverableAuthException e) {
Log.e(TAG, "Exception", e); SyncUtils.sendNotification(context, account.name, e.getIntent());
} catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e);
} catch (UserRecoverableAuthIOException e) {
SyncUtils.sendNotification(context, account.name, e.getIntent());
} catch (IOException e) {
Log.e(TAG, "IOException", e);
} }
} }
@@ -127,24 +140,45 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* *
* @param folderId the folder id * @param folderId the folder id
*/ */
private void performInitialSync(String folderId) throws Exception { private void performInitialSync(String folderId) throws IOException {
// Get the largest change id first to avoid race conditions // Get the largest change id first to avoid race conditions
About about = drive.about().get().setFields(ABOUT_GET_FIELDS).execute(); About about = drive.about().get().setFields(ABOUT_GET_FIELDS).execute();
long largestChangeId = about.getLargestChangeId(); long largestChangeId = about.getLargestChangeId();
// Get all the KML files in the "My Drive:/My Tracks" folder // Get all the KML files in the "My Drive:/My Tracks" folder
Files.List request = drive.files().list() Files.List myTracksFolderRequest = drive.files()
.setQ(String.format(Locale.US, SyncUtils.MY_TRACKS_FOLDER_FILES_QUERY, folderId)); .list().setQ(String.format(Locale.US, SyncUtils.MY_TRACKS_FOLDER_FILES_QUERY, folderId));
Map<String, File> idToFileMap = getFiles(request); Map<String, File> myTracksFolderMap = getFiles(myTracksFolderRequest);
insertNewDriveFiles(idToFileMap.values());
// Get all the KML files in the "Shared with me:/" folder // Get all the KML files in the "Shared with me:/" folder
request = drive.files().list().setQ(SyncUtils.SHARED_WITH_ME_FILES_QUERY); Files.List sharedWithMeRequest = drive.files()
idToFileMap = getFiles(request); .list().setQ(SyncUtils.SHARED_WITH_ME_FILES_QUERY);
insertNewDriveFiles(idToFileMap.values()); Map<String, File> sharedWithMeMap = getFiles(sharedWithMeRequest);
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId); try {
insertNewDriveFiles(myTracksFolderMap.values());
insertNewDriveFiles(sharedWithMeMap.values());
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId);
} catch (IOException e) {
// Remove all imported tracks
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.DRIVE_ID_TRACKS_QUERY, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
myTracksProviderUtils.deleteTrack(track.getId());
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
throw e;
}
} }
/** /**
@@ -153,26 +187,13 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* @param folderId the folder id * @param folderId the folder id
* @param largestChangeId the largest change id * @param largestChangeId the largest change id
*/ */
private void performIncrementalSync(String folderId, long largestChangeId) throws Exception { private void performIncrementalSync(String folderId, long largestChangeId) throws IOException {
// Move to trash in Drive all deleted tracks
String driveDeletedList = PreferencesUtils.getString( String driveDeletedList = PreferencesUtils.getString(
context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT); context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
String deletedIds[] = TextUtils.split(driveDeletedList, ";"); String deletedIds[] = TextUtils.split(driveDeletedList, ";");
for (String id : deletedIds) { for (String driveId : deletedIds) {
try { deleteDriveFile(driveId, folderId, true);
File driveFile = drive.files().get(id).execute();
if (SyncUtils.isInFolder(driveFile, folderId)) {
if (!driveFile.getLabels().getTrashed()) {
drive.files().trash(id).execute();
}
// if trashed, ignore
} else if (SyncUtils.isSharedWithMe(driveFile)) {
drive.files().delete(id).execute();
}
} catch (IOException e) {
// safe to ignore
}
} }
PreferencesUtils.setString( PreferencesUtils.setString(
context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT); context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
@@ -197,7 +218,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
Log.d(TAG, "Delete local track " + track.getName()); Log.d(TAG, "Delete local track " + track.getName());
myTracksProviderUtils.deleteTrack(track.getId()); myTracksProviderUtils.deleteTrack(track.getId());
} else { } else {
mergeFiles(track, driveFile); merge(track, driveFile);
} }
changes.remove(driveId); changes.remove(driveId);
} else { } else {
@@ -206,7 +227,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
// Handle the case the track has changed // Handle the case the track has changed
File driveFile = drive.files().get(driveId).execute(); File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isValid(driveFile, folderId)) { if (SyncUtils.isValid(driveFile, folderId)) {
mergeFiles(track, driveFile); merge(track, driveFile);
} else { } else {
/* /*
* Track has a drive id, but the drive id is no longer valid. * Track has a drive id, but the drive id is no longer valid.
@@ -225,10 +246,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
// Handle new drive files // Handle new drive files
insertNewDriveFiles(changes.values()); insertNewDriveFiles(changes.values());
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId); PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException", e);
} finally { } finally {
if (cursor != null) { if (cursor != null) {
cursor.close(); cursor.close();
@@ -253,8 +270,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
if (track.getId() == recordingTrackId) { if (track.getId() == recordingTrackId) {
continue; continue;
} }
// Note, will retry on the next sync if unable to add drive file // If not successful, the next sync will retry again
SyncUtils.addDriveFile(context, myTracksProviderUtils, drive, folderId, track); SyncUtils.insertDriveFile(drive, folderId, context, myTracksProviderUtils, track, true);
} while (cursor.moveToNext()); } while (cursor.moveToNext());
} }
} finally { } finally {
@@ -268,24 +285,36 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* Inserts new Google Drive files. * Inserts new Google Drive files.
* *
* @param driveFiles a collection of drive files to insert * @param driveFiles a collection of drive files to insert
* @throws IOException
*/ */
private void insertNewDriveFiles(Collection<File> driveFiles) throws Exception { private void insertNewDriveFiles(Collection<File> driveFiles) throws IOException {
for (File driveFile : driveFiles) { for (File driveFile : driveFiles) {
if (driveFile != null) { if (driveFile != null) {
InputStream inputStream = downloadDriveFile(driveFile); InputStream inputStream = downloadDriveFile(driveFile, true);
// TODO: should retry if inputStream is null
if (inputStream != null) { if (inputStream != null) {
KmlImporter kmlImporter = new KmlImporter(context, -1L); KmlImporter kmlImporter = new KmlImporter(context, -1L);
long[] tracksIds = kmlImporter.importFile(inputStream); try {
if (tracksIds.length == 1) { long[] trackIds = kmlImporter.importFile(inputStream);
Track track = myTracksProviderUtils.getTrack(tracksIds[0]); if (trackIds.length == 1) {
track.setDriveId(driveFile.getId()); Track track = myTracksProviderUtils.getTrack(trackIds[0]);
track.setModifiedTime(driveFile.getModifiedDate().getValue()); track.setDriveId(driveFile.getId());
track.setSharedWithMe(driveFile.getSharedWithMeDate() != null); track.setModifiedTime(driveFile.getModifiedDate().getValue());
myTracksProviderUtils.updateTrack(track); track.setSharedWithMe(driveFile.getSharedWithMeDate() != null);
Log.d(TAG, "Add from Google Drive " + track.getName()); myTracksProviderUtils.updateTrack(track);
Log.d(TAG, "Add from Google Drive " + track.getName());
} else {
// Clean up if imported more than one track
for (int i = 0; i < trackIds.length; i++) {
myTracksProviderUtils.deleteTrack(trackIds[i]);
}
}
} catch (SAXException e) {
Log.e(TAG, "Unable to insert new drive file for " + driveFile.getId(), e);
} catch (IOException e) {
Log.e(TAG, "Unable to insert new drive file for " + driveFile.getId(), e);
} catch (ParserConfigurationException e) {
Log.e(TAG, "Unable to insert new drive file for " + driveFile.getId(), e);
} }
// Ignore if tracksId.length != 1
} }
} }
} }
@@ -297,20 +326,15 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* @param request the request * @param request the request
* @return a map of file id to file * @return a map of file id to file
*/ */
private Map<String, File> getFiles(Files.List request) { private Map<String, File> getFiles(Files.List request) throws IOException {
Map<String, File> idToFileMap = new HashMap<String, File>(); Map<String, File> idToFileMap = new HashMap<String, File>();
do { do {
try { FileList files = request.execute();
FileList files = request.execute();
for (File file : files.getItems()) { for (File file : files.getItems()) {
idToFileMap.put(file.getId(), file); idToFileMap.put(file.getId(), file);
}
request.setPageToken(files.getNextPageToken());
} catch (IOException e) {
Log.e(TAG, "IOException", e);
request.setPageToken(null);
} }
request.setPageToken(files.getNextPageToken());
} while (request.getPageToken() != null && request.getPageToken().length() > 0); } while (request.getPageToken() != null && request.getPageToken().length() > 0);
return idToFileMap; return idToFileMap;
} }
@@ -323,38 +347,35 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* @param changes a map of drive id to file for the changes * @param changes a map of drive id to file for the changes
* @return an updated largest change id * @return an updated largest change id
*/ */
private long getDriveChanges(String folderId, long changeId, Map<String, File> changes) { private long getDriveChanges(String folderId, long changeId, Map<String, File> changes)
try { throws IOException {
Changes.List request = drive.changes().list().setStartChangeId(changeId + 1); Changes.List request = drive.changes().list().setStartChangeId(changeId + 1);
do { do {
ChangeList changeList = request.execute(); ChangeList changeList = request.execute();
long newId = changeList.getLargestChangeId().longValue(); long newId = changeList.getLargestChangeId().longValue();
for (Change change : changeList.getItems()) { for (Change change : changeList.getItems()) {
if (change.getDeleted()) { if (change.getDeleted()) {
changes.put(change.getFileId(), null); changes.put(change.getFileId(), null);
} else { } else {
File file = change.getFile(); File file = change.getFile();
if (SyncUtils.isInFolder(file, folderId)) { if (SyncUtils.isInFolder(file, folderId)) {
if (file.getLabels().getTrashed()) { if (file.getLabels().getTrashed()) {
changes.put(change.getFileId(), null); changes.put(change.getFileId(), null);
} else { } else {
changes.put(change.getFileId(), file);
}
} else if (SyncUtils.isSharedWithMe(file)) {
changes.put(change.getFileId(), file); changes.put(change.getFileId(), file);
} }
} else if (SyncUtils.isSharedWithMe(file)) {
changes.put(change.getFileId(), file);
} }
} }
if (newId > changeId) { }
changeId = newId; if (newId > changeId) {
} changeId = newId;
request.setPageToken(changeList.getNextPageToken()); }
} while (request.getPageToken() != null && request.getPageToken().length() > 0); request.setPageToken(changeList.getNextPageToken());
} catch (IOException e) { } while (request.getPageToken() != null && request.getPageToken().length() > 0);
Log.e(TAG, "IOException", e); Log.d(TAG, "Got drive changes: " + changes.size() + " " + changeId);
}
Log.d(TAG, "Got drive changes: " + changes.size() + " - " + changeId);
return changeId; return changeId;
} }
@@ -364,44 +385,77 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* @param track the track * @param track the track
* @param driveFile the drive file * @param driveFile the drive file
*/ */
private void mergeFiles(Track track, File driveFile) throws Exception { private void merge(Track track, File driveFile) throws IOException {
long modifiedTime = track.getModifiedTime(); long modifiedTime = track.getModifiedTime();
long driveModifiedTime = driveFile.getModifiedDate().getValue(); long driveModifiedTime = driveFile.getModifiedDate().getValue();
if (modifiedTime > driveModifiedTime) { if (modifiedTime > driveModifiedTime) {
Log.d(TAG, "Updating track change " + track.getName()); Log.d(TAG, "Updating track change for track " + track.getName() + " and drive file "
if (!SyncUtils.updateDriveFile(context, myTracksProviderUtils, drive, driveFile, track)) { + driveFile.getOriginalFilename());
if (!SyncUtils.updateDriveFile(
// TODO: Should inform the user if cannot update the file drive, driveFile, context, myTracksProviderUtils, track, true)) {
Log.e(TAG, "Unable to update drive file");
track.setModifiedTime(driveModifiedTime); track.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
} }
} else if (modifiedTime < driveModifiedTime) { } else if (modifiedTime < driveModifiedTime) {
Log.d(TAG, "Updating drive change " + track.getName()); Log.d(TAG, "Updating drive change for track " + track.getName() + " and drive file "
InputStream inputStream = downloadDriveFile(driveFile); + driveFile.getOriginalFilename());
if (inputStream == null) { InputStream inputStream = downloadDriveFile(driveFile, true);
if (inputStream != null) {
// TODO: Should retry if cannot download
Log.e(TAG, "Unable to update drive change. Input stream is null for " + track.getName());
track.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(track);
} else {
KmlImporter kmlImporter = new KmlImporter(context, track.getId()); KmlImporter kmlImporter = new KmlImporter(context, track.getId());
long[] tracksIds = kmlImporter.importFile(inputStream); try {
if (tracksIds.length == 1) { long[] trackIds = kmlImporter.importFile(inputStream);
Track newTrack = myTracksProviderUtils.getTrack(tracksIds[0]); if (trackIds.length == 1) {
newTrack.setDriveId(driveFile.getId()); Track newTrack = myTracksProviderUtils.getTrack(trackIds[0]);
newTrack.setModifiedTime(driveModifiedTime); newTrack.setDriveId(driveFile.getId());
newTrack.setSharedWithMe(driveFile.getSharedWithMeDate() != null); newTrack.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(newTrack); newTrack.setSharedWithMe(driveFile.getSharedWithMeDate() != null);
} else { myTracksProviderUtils.updateTrack(newTrack);
return;
/* } else {
* TODO: Should revert the track back to the original. Log.e(TAG, "Unable to merge, imported size is not 1");
*/ }
Log.e( } catch (SAXException e) {
TAG, "Unable to update drive change. Imported size is not 1 for " + track.getName()); Log.e(TAG, "Unable to merge", e);
} catch (ParserConfigurationException e) {
Log.e(TAG, "Unable to merge", e);
} catch (IOException e) {
Log.e(TAG, "Unable to merge", e);
} }
} }
Log.e(TAG, "Unable to update drive change");
track.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(track);
}
}
/**
* Deletes a drive file.
*
* @param driveId the drive id
* @param folderId the folder id
* @param canRetry true if can retry the request
*/
private void deleteDriveFile(String driveId, String folderId, boolean canRetry)
throws UserRecoverableAuthIOException {
try {
File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isInFolder(driveFile, folderId)) {
if (!driveFile.getLabels().getTrashed()) {
drive.files().trash(driveId).execute();
}
// if trashed, ignore
} else if (SyncUtils.isSharedWithMe(driveFile)) {
drive.files().delete(driveId).execute();
}
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
deleteDriveFile(driveId, folderId, false);
return;
}
Log.e(TAG, "Unable to delete Drive file for " + driveId, e);
} }
} }
@@ -410,24 +464,26 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
* *
* @param driveFile the drive file * @param driveFile the drive file
*/ */
private InputStream downloadDriveFile(File driveFile) { private InputStream downloadDriveFile(File driveFile, boolean canRetry) throws IOException {
if (driveFile.getDownloadUrl() != null && driveFile.getDownloadUrl().length() > 0) { if (driveFile.getDownloadUrl() == null || driveFile.getDownloadUrl().length() == 0) {
try {
HttpResponse httpResponse = drive.getRequestFactory()
.buildGetRequest(new GenericUrl(driveFile.getDownloadUrl())).execute();
if (httpResponse != null) {
return httpResponse.getContent();
} else {
Log.e(TAG, "http response is null");
return null;
}
} catch (IOException e) {
Log.e(TAG, "IOException", e);
return null;
}
} else {
Log.d(TAG, "Drive file download url doesn't exist: " + driveFile.getTitle()); Log.d(TAG, "Drive file download url doesn't exist: " + driveFile.getTitle());
return null; return null;
} }
try {
HttpResponse httpResponse = drive.getRequestFactory()
.buildGetRequest(new GenericUrl(driveFile.getDownloadUrl())).execute();
if (httpResponse == null) {
Log.e(TAG, "http response is null");
return null;
}
return httpResponse.getContent();
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return downloadDriveFile(driveFile, false);
}
throw e;
}
} }
} }
@@ -86,7 +86,7 @@ public class SyncUtils {
// Get My Tracks folder KML files // Get My Tracks folder KML files
public static final String MY_TRACKS_FOLDER_FILES_QUERY = "'%s' in parents and mimeType = '" public static final String MY_TRACKS_FOLDER_FILES_QUERY = "'%s' in parents and mimeType = '"
+ KML_MIME_TYPE + "' and trashed = false and not sharedWithMe"; + KML_MIME_TYPE + "' and trashed = false";
// Get shared with me KML files // Get shared with me KML files
public static final String SHARED_WITH_ME_FILES_QUERY = "sharedWithMe and mimeType = '" public static final String SHARED_WITH_ME_FILES_QUERY = "sharedWithMe and mimeType = '"
@@ -107,6 +107,21 @@ public class SyncUtils {
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 * Checks permission by an activity. Will start an activity to request
* permission with request code {@link #DRIVE_PERMISSION_REQUEST_CODE}. * permission with request code {@link #DRIVE_PERMISSION_REQUEST_CODE}.
@@ -130,7 +145,7 @@ public class SyncUtils {
}); });
} catch (UserRecoverableAuthException e) { } catch (UserRecoverableAuthException e) {
activity.startActivityForResult(e.getIntent(), DRIVE_PERMISSION_REQUEST_CODE); activity.startActivityForResult(e.getIntent(), DRIVE_PERMISSION_REQUEST_CODE);
} catch (GoogleAuthException e) { } catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e); Log.e(TAG, "GoogleAuthException", e);
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
@@ -155,24 +170,26 @@ public class SyncUtils {
} }
/** /**
* Gets the drive credential. Needs to be run in a background thread. * Sends a notification to request permission.
* *
* @param context the context * @param context the context
* @param accountName the account name * @param accountName the account name
* @param intent the intent
*/ */
public static GoogleAccountCredential getCredential(Context context, String accountName) { public static void sendNotification(Context context, String accountName, Intent intent) {
try { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).addFlags(Intent.FLAG_FROM_BACKGROUND);
return getGoogleAccountCredential(context, accountName);
} catch (UserRecoverableAuthException e) { PendingIntent pendingIntent = PendingIntent.getActivity(
sendNotification(context, accountName, e.getIntent()); context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
} catch (GoogleAuthException e) { NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setAutoCancel(true)
Log.e(TAG, "GoogleAuthException", e); .setContentIntent(pendingIntent)
} catch (UserRecoverableAuthIOException e) { .setContentText(context.getString(R.string.permission_request_message, accountName))
sendNotification(context, accountName, e.getIntent()); .setContentTitle(context.getString(R.string.permission_request_title))
} catch (IOException e) { .setSmallIcon(android.R.drawable.ic_dialog_alert)
Log.e(TAG, "IOException", e); .setTicker(context.getString(R.string.permission_request_title));
} NotificationManager notificationManager = (NotificationManager) context.getSystemService(
return null; Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, builder.build());
} }
/** /**
@@ -310,17 +327,19 @@ public class SyncUtils {
} }
/** /**
* Adds a Drive file. * Inserts a Drive file.
* *
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param drive the drive * @param drive the drive
* @param folderId the folder id * @param folderId the folder id
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track * @param track the track
* @param canRetry true if can retry
* @return the added drive file id or null. * @return the added drive file id or null.
*/ */
public static String addDriveFile(Context context, MyTracksProviderUtils myTracksProviderUtils, public static String insertDriveFile(Drive drive, String folderId, Context context,
Drive drive, String folderId, Track track) throws IOException { MyTracksProviderUtils myTracksProviderUtils, Track track, boolean canRetry)
throws IOException {
java.io.File file = getFile(context, myTracksProviderUtils, track); java.io.File file = getFile(context, myTracksProviderUtils, track);
if (file == null) { if (file == null) {
Log.e(TAG, "Unable to add Drive file. File is null for track " + track.getName()); Log.e(TAG, "Unable to add Drive file. File is null for track " + track.getName());
@@ -329,21 +348,7 @@ public class SyncUtils {
try { try {
Log.d(TAG, "Add Drive file for track " + track.getName()); Log.d(TAG, "Add Drive file for track " + track.getName());
FileContent fileContent = new FileContent(KML_MIME_TYPE, file); File uploadedFile = insertDriveFile(drive, folderId, track.getName(), file, canRetry);
// file's parent
ParentReference parentReference = new ParentReference();
parentReference.setId(folderId);
ArrayList<ParentReference> parents = new ArrayList<ParentReference>();
parents.add(parentReference);
// file's metadata
File newMetaData = new File();
newMetaData.setTitle(track.getName() + "." + TrackFileFormat.KML.getExtension());
newMetaData.setMimeType(KML_MIME_TYPE);
newMetaData.setParents(parents);
File uploadedFile = drive.files().insert(newMetaData, fileContent).execute();
if (uploadedFile == null) { if (uploadedFile == null) {
Log.e(TAG, "Unable to add Drive file. Uploaded file is null for track " + track.getName()); Log.e(TAG, "Unable to add Drive file. Uploaded file is null for track " + track.getName());
return null; return null;
@@ -360,16 +365,55 @@ public class SyncUtils {
} }
/** /**
* Updates a Drive file. * Inserts a Drive file.
*
* @param drive the drive
* @param folderId the folder id
* @param name the track name
* @param file the track file
* @param canRetry true if can retry
*/
private static File insertDriveFile(
Drive drive, String folderId, String name, java.io.File file, boolean canRetry)
throws IOException {
try {
FileContent fileContent = new FileContent(KML_MIME_TYPE, file);
// file's parent
ParentReference parentReference = new ParentReference();
parentReference.setId(folderId);
ArrayList<ParentReference> parents = new ArrayList<ParentReference>();
parents.add(parentReference);
// file's metadata
File newMetaData = new File();
newMetaData.setTitle(name + "." + TrackFileFormat.KML.getExtension());
newMetaData.setMimeType(KML_MIME_TYPE);
newMetaData.setParents(parents);
return drive.files().insert(newMetaData, fileContent).execute();
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return insertDriveFile(drive, folderId, name, file, false);
}
throw e;
}
}
/**
* Updates a Drive file. Returns true if successful.
* *
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param drive the drive * @param drive the drive
* @param driveFile the drive file * @param driveFile the drive file
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track * @param track the track
* @param canRetry true if can retry
*/ */
public static boolean updateDriveFile(Context context, public static boolean updateDriveFile(Drive drive, File driveFile, Context context,
MyTracksProviderUtils myTracksProviderUtils, Drive drive, File driveFile, Track track) MyTracksProviderUtils myTracksProviderUtils, Track track, boolean canRetry)
throws IOException { throws IOException {
Log.d(TAG, "Update drive file for track " + track.getName()); Log.d(TAG, "Update drive file for track " + track.getName());
java.io.File file = SyncUtils.getFile(context, myTracksProviderUtils, track); java.io.File file = SyncUtils.getFile(context, myTracksProviderUtils, track);
@@ -379,18 +423,13 @@ public class SyncUtils {
return false; return false;
} else { } else {
try { try {
FileContent fileContent = new FileContent(KML_MIME_TYPE, file); File updatedFile = updateDriveFile(drive, driveFile, track.getName(), file, canRetry);
driveFile.setTitle(track.getName() + "." + TrackFileFormat.KML.getExtension());
File updatedFile = drive.files()
.update(driveFile.getId(), driveFile, fileContent).execute();
if (updatedFile == null) { if (updatedFile == null) {
Log.e(TAG, Log.e(TAG,
"Unable to update drive file. Updated file is null for track " + track.getName()); "Unable to update drive file. Updated file is null for track " + track.getName());
return false; return false;
} }
long newModifiedTime = updatedFile.getModifiedDate().getValue(); track.setModifiedTime(updatedFile.getModifiedDate().getValue());
track.setModifiedTime(newModifiedTime);
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
return true; return true;
} finally { } finally {
@@ -400,43 +439,31 @@ public class SyncUtils {
} }
/** /**
* Gets the google account credential. * Updates a Drive file.
* *
* @param context the context * @param drive the drive
* @param accountName the account name * @param driveFile the drive file
* @param name the track name
* @param file the track file
* @param canRetry true if can retry
*/ */
public static GoogleAccountCredential getGoogleAccountCredential( private static File updateDriveFile(
Context context, String accountName) throws IOException, GoogleAuthException { Drive drive, File driveFile, String name, java.io.File file, boolean canRetry)
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2( throws IOException {
context, DriveScopes.DRIVE); try {
credential.setSelectedAccountName(accountName); FileContent fileContent = new FileContent(KML_MIME_TYPE, file);
credential.getToken(); driveFile.setTitle(name + "." + TrackFileFormat.KML.getExtension());
return credential; return drive.files().update(driveFile.getId(), driveFile, fileContent).execute();
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return updateDriveFile(drive, driveFile, name, file, false);
}
throw e;
}
} }
/**
* 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());
}
/** /**
* Gets a file from a track. * Gets a file from a track.
* *
@@ -24,8 +24,6 @@ import com.google.api.services.drive.Drive;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
import java.io.IOException;
/** /**
* Tests the situation when user use multiple account in MyTracks. * Tests the situation when user use multiple account in MyTracks.
* *
@@ -63,10 +61,8 @@ public class MultiAccountsSyncTest extends ActivityInstrumentationTestCase2<Trac
* <li>8. Enable sync with account2.</li> * <li>8. Enable sync with account2.</li>
* <li>9. Check</li> * <li>9. Check</li>
* </ul> * </ul>
*
* @throws IOException
*/ */
public void testSyncTracksWithMultiAccounts() throws IOException { public void testSyncTracksWithMultiAccounts() throws Exception {
if (!SyncTestUtils.runSyncTest) { if (!SyncTestUtils.runSyncTest) {
return; return;
} }
@@ -108,10 +104,8 @@ public class MultiAccountsSyncTest extends ActivityInstrumentationTestCase2<Trac
* <li>5. Sync with account 1 (account 1 should have 1 file, account2 should * <li>5. Sync with account 1 (account 1 should have 1 file, account2 should
* also have 1 file).</li> * also have 1 file).</li>
* </ul> * </ul>
*
* @throws IOException
*/ */
public void testDeleteTracksWithMultiAccounts() throws IOException { public void testDeleteTracksWithMultiAccounts() throws Exception {
if (!SyncTestUtils.runSyncTest) { if (!SyncTestUtils.runSyncTest) {
return; return;
} }
@@ -20,6 +20,7 @@ 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.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.maps.mytracks.R; 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.GoogleAccountCredential;
import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.googleapis.json.GoogleJsonResponseException;
@@ -61,10 +62,9 @@ public class SyncTestUtils {
* @param instrumentation the instrumentation is used for test * @param instrumentation the instrumentation is used for test
* @param trackListActivity the startup activity * @param trackListActivity the startup activity
* @return a Google Drive object * @return a Google Drive object
* @throws IOException
*/ */
public static Drive setUpForSyncTest(Instrumentation instrumentation, public static Drive setUpForSyncTest(Instrumentation instrumentation,
TrackListActivity trackListActivity) throws IOException { TrackListActivity trackListActivity) throws IOException, GoogleAuthException {
if (runSyncTest || !isCheckedRunSyncTest) { if (runSyncTest || !isCheckedRunSyncTest) {
EndToEndTestUtils.setupForAllTest(instrumentation, trackListActivity); EndToEndTestUtils.setupForAllTest(instrumentation, trackListActivity);
} }
@@ -116,10 +116,11 @@ public class SyncTestUtils {
* @param context the context of application * @param context the context of application
* @return a Google Drive object * @return a Google Drive object
*/ */
public static Drive getGoogleDrive(Context context) { public static Drive getGoogleDrive(Context context) throws IOException, GoogleAuthException {
String googleAccount = PreferencesUtils.getString(context, R.string.google_account_key, String googleAccount = PreferencesUtils.getString(
PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT); context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
GoogleAccountCredential credential = SyncUtils.getCredential(context, googleAccount); GoogleAccountCredential credential = SyncUtils.getGoogleAccountCredential(
context, googleAccount);
return SyncUtils.getDriveService(credential); return SyncUtils.getDriveService(credential);
} }