/* * This file is part of GPSLogger for Android. * * GPSLogger for Android is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * GPSLogger for Android is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GPSLogger for Android. If not, see <http://www.gnu.org/licenses/>. */ package com.mendhak.gpslogger.senders.gdocs; import android.accounts.*; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import com.mendhak.gpslogger.common.IActionListener; import com.mendhak.gpslogger.common.Utilities; import com.mendhak.gpslogger.senders.IFileSender; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; public class GDocsHelper implements IActionListener, IFileSender { Context ctx; IActionListener callback; public GDocsHelper(Context applicationContext, IActionListener callback) { this.ctx = applicationContext; this.callback = callback; } /** * OAuth 2 scope to use */ //https://docs.google.com/feeds/ gives full access to the user's documents private static final String SCOPE = "oauth2:https://docs.google.com/feeds/"; /** * Returns the Google API CLIENT ID to use in API calls * * @param applicationContext * @return */ private static String GetClientID(Context applicationContext) { int RClientId = applicationContext.getResources().getIdentifier( "gdocs_clientid", "string", applicationContext.getPackageName()); return applicationContext.getString(RClientId); } /** * Returns the Google API CLIENT SECRET to use in API calls * * @param applicationContext * @return */ private static String GetClientSecret(Context applicationContext) { int RClientSecret = applicationContext.getResources().getIdentifier( "gdocs_clientsecret", "string", applicationContext.getPackageName()); return applicationContext.getString(RClientSecret); } /** * Gets the stored authToken, which may be expired */ public static String GetAuthToken(Context applicationContext) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); return prefs.getString("GDOCS_AUTH_TOKEN", ""); } /** * Gets the stored account name */ public static String GetAccountName(Context applicationContext) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); return prefs.getString("GDOCS_ACCOUNT_NAME", ""); } /** * Saves the authToken and account name into shared preferences */ public static void SaveAuthToken(Context applicationContext, AccountManagerFuture<Bundle> bundleAccountManagerFuture) { try { String authToken = bundleAccountManagerFuture.getResult().getString(AccountManager.KEY_AUTHTOKEN); String accountName = bundleAccountManagerFuture.getResult().getString(AccountManager.KEY_ACCOUNT_NAME); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); SharedPreferences.Editor editor = prefs.edit(); Utilities.LogDebug("Saving GDocs authToken: " + authToken); editor.putString("GDOCS_AUTH_TOKEN", authToken); editor.putString("GDOCS_ACCOUNT_NAME", accountName); editor.commit(); } catch (Exception e) { Utilities.LogError("GDocsHelper.SaveAuthToken", e); } } /** * Removes the authToken and account name from storage * * @param applicationContext */ public static void ClearAuthToken(Context applicationContext) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); SharedPreferences.Editor editor = prefs.edit(); editor.remove("GDOCS_AUTH_TOKEN"); editor.remove("GDOCS_ACCOUNT_NAME"); editor.commit(); } /** * Returns whether the app is authorized to perform Google API operations * * @param applicationContext * @return */ public static boolean IsLinked(Context applicationContext) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext); String gdocsAuthToken = prefs.getString("GDOCS_AUTH_TOKEN", ""); String gdocsAccount = prefs.getString("GDOCS_ACCOUNT_NAME", ""); return gdocsAuthToken.length() > 0 && gdocsAccount.length() > 0; } /** * Gets an instance of an AccountManager to use for authorizing the app * * @param applicationContext * @return */ public static AccountManager GetAccountManager(Context applicationContext) { return AccountManager.get(applicationContext); } /** * This version show the user an access request screen for the user to authorize the app * * @param accountManager * @param account * @param ota * @param activity */ public static void GetAuthTokenFromAccountManager(AccountManager accountManager, Account account, AccountManagerCallback<Bundle> ota, Activity activity) { Bundle bundle = new Bundle(); accountManager.getAuthToken(account, SCOPE, bundle, activity, ota, null); } /** * This version puts a message in the notification area asking for authorization * * @param accountManager * @param account * @param ota */ public static void GetAuthTokenFromAccountManager(AccountManager accountManager, Account account, AccountManagerCallback<Bundle> ota) { accountManager.getAuthToken( account, // Account retrieved using getAccountsByType() SCOPE, // Auth scope true, ota, // Callback called when a token is successfully acquired null); // Callback called if an error occurs } /** * Invalidates the authToken and requests a new one and saves the new authToken * * @param applicationContext * @param accountManager */ private void ResetAuthToken(final Context applicationContext, final AccountManager accountManager, final Thread threadToStart) { //To completely revoke access, adb -e shell 'sqlite3 /data/system/accounts.db "delete from grants;"' //Invalidate token, get new token, invalidate it again, then get it again. //As weird as that sounds, the first time you get a token it will be expired. Account[] accounts = accountManager.getAccountsByType("com.google"); Account account = null; for (Account acc : accounts) { if (acc.name.equalsIgnoreCase(GetAccountName(applicationContext))) { account = acc; } } final Account finalAccount = account; //Invalidate the token accountManager.invalidateAuthToken("com.google", GetAuthToken(applicationContext)); //Request a token (AccountManager will return a cached and probably expired token) GetAuthTokenFromAccountManager(accountManager, account, new AccountManagerCallback<Bundle>() { // @Override public void run(AccountManagerFuture<Bundle> bundleAccountManagerFuture) { //Save the (stale) token SaveAuthToken(applicationContext, bundleAccountManagerFuture); //Invalidate it again accountManager.invalidateAuthToken("com.google", GetAuthToken(applicationContext)); //Request a token again GetAuthTokenFromAccountManager(accountManager, finalAccount, new AccountManagerCallback<Bundle>() { // @Override public void run(AccountManagerFuture<Bundle> bundleAccountManagerFuture) { //and finally save it SaveAuthToken(applicationContext, bundleAccountManagerFuture); threadToStart.start(); } }); } }); } public static Account[] GetAccounts(AccountManager accountManager) { return accountManager.getAccountsByType("com.google"); } public void UploadTestFile() { if (!IsLinked(ctx)) { callback.OnFailure(); return; } try { AccountManager accountManager = GetAccountManager(ctx); Thread t = new Thread(new GDocsUploadHandler( new ByteArrayInputStream("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<a>This is a test upload</a>".getBytes()), "test.xml", GDocsHelper.this)); ResetAuthToken(ctx, accountManager, t); } catch (Exception e) { callback.OnFailure(); Utilities.LogError("GDocsHelper.UploadTestFile", e); } } // @Override public void OnComplete() { callback.OnComplete(); } // @Override public void OnFailure() { callback.OnFailure(); } // @Override public void UploadFile(List<File> files) { //If there's a zip file, upload just that //Else upload everything in files. File zipFile = null; for (File f : files) { if (f.getName().contains(".zip")) { zipFile = f; break; } } if (zipFile != null) { UploadFile(zipFile.getName()); } else { for (File f : files) { UploadFile(f.getName()); } } } public void UploadFile(final String fileName) { if (!IsLinked(ctx)) { callback.OnFailure(); return; } try { AccountManager accountManager = GetAccountManager(ctx); File gpsDir = new File(Environment.getExternalStorageDirectory(), "GPSLogger"); File gpxFile = new File(gpsDir, fileName); FileInputStream fis = new FileInputStream(gpxFile); Thread t = new Thread(new GDocsUploadHandler(fis, fileName, GDocsHelper.this)); ResetAuthToken(ctx, accountManager, t); } catch (Exception e) { callback.OnFailure(); Utilities.LogError("GDocsHelper.UploadFile", e); } } // @Override public boolean accept(File dir, String name) { return name.toLowerCase().endsWith(".zip") || name.toLowerCase().endsWith(".gpx") || name.toLowerCase().endsWith(".kml"); } private class GDocsUploadHandler implements Runnable { String fileName; InputStream inputStream; IActionListener callback; GDocsUploadHandler(InputStream inputStream, String fileName, IActionListener callback) { this.inputStream = inputStream; this.fileName = fileName; this.callback = callback; } // @Override public void run() { try { if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) { //Due to a pre-froyo bug //http://android-developers.blogspot.com/2011/09/androids-http-clients.html System.setProperty("http.keepAlive", "false"); } String gpsLoggerFolderFeed = SearchForGpsLoggerFolder(); if (Utilities.IsNullOrEmpty(gpsLoggerFolderFeed)) { //Couldn't find anything, need to create it. gpsLoggerFolderFeed = CreateFolder(); } FileAccessLocations fileSearch = SearchForFile(gpsLoggerFolderFeed, fileName); if (Utilities.IsNullOrEmpty(fileSearch.UpdateUrl)) { //The file doesn't exist, you must create it. CreateFile(fileSearch, fileName, Utilities.GetByteArrayFromInputStream(inputStream)); } else { //The file exists, update its contents instead UpdateFile(fileSearch, fileName, Utilities.GetByteArrayFromInputStream(inputStream)); } callback.OnComplete(); } catch (Exception e) { Utilities.LogError("GDocsUploadHandler.run", e); callback.OnFailure(); } } private void UpdateFile(FileAccessLocations accessLocations, String fileName, byte[] fileContents) { String resumableFileUploadUrl = UploadFileContentsToResumableUrl(accessLocations.UpdateUrl + "?convert=false", fileName, fileContents, true); UploadFileContentsToResumableUrl(resumableFileUploadUrl, fileName, fileContents, true); } private void CreateFile(FileAccessLocations accessLocations, String fileName, byte[] fileContents) { String createFileAtomXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:docs=\"http://schemas.google.com/docs/2007\">\n" + " <category scheme=\"http://schemas.google.com/g/2005#kind\"\n" + " term=\"http://schemas.google.com/docs/2007#document\"/>\n" + " <title>" + fileName + "</title>\n" + "</entry>"; String resumableFileUploadUrl = UploadFileContentsToResumableUrl(accessLocations.CreateUrl + "?convert=false", fileName, createFileAtomXml.getBytes(), false); UploadFileContentsToResumableUrl(resumableFileUploadUrl, fileName, fileContents, false); } private String UploadFileContentsToResumableUrl(String resumableFileUploadUrl, String fileName, byte[] fileContents, boolean isUpdate) { //This method gets used 4 times - to get the resumable location for create/edit, and to do the actual uploads. String newLocation = ""; HttpURLConnection conn = null; try { URL url = new URL(resumableFileUploadUrl); conn = (HttpURLConnection) url.openConnection(); AddCommonHeaders(conn); conn.setRequestProperty("X-Upload-Content-Length", String.valueOf(fileContents.length)); //back to 0 conn.setRequestProperty("X-Upload-Content-Type", Utilities.GetMimeTypeFromFileName(fileName)); conn.setRequestProperty("Content-Type", Utilities.GetMimeTypeFromFileName(fileName)); conn.setRequestProperty("Content-Length", String.valueOf(fileContents.length)); conn.setRequestProperty("Slug", fileName); if (isUpdate) { conn.setRequestProperty("If-Match", "*"); conn.setRequestMethod("PUT"); } else { conn.setRequestMethod("POST"); } conn.setUseCaches(false); conn.setDoInput(true); conn.setDoOutput(true); DataOutputStream wr = new DataOutputStream( conn.getOutputStream()); //wr.writeBytes(fileContents); wr.write(fileContents); wr.flush(); wr.close(); //Make the request conn.getResponseCode(); newLocation = conn.getHeaderField("location"); } catch (Exception e) { Utilities.LogError("GDocsUploadHandler.UploadFileContentsToResumableUrl", e); } finally { if (conn != null) { conn.disconnect(); } } return newLocation; } private FileAccessLocations SearchForFile(String gpsLoggerFolderFeed, String fileName) { FileAccessLocations fal = new FileAccessLocations(); HttpURLConnection conn = null; String searchUrl = gpsLoggerFolderFeed + "?title=" + fileName; try { URL url = new URL(searchUrl); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); AddCommonHeaders(conn); Document doc = Utilities.GetDocumentFromInputStream(conn.getInputStream()); fal.CreateUrl = GetFileUploadUrl(doc); fal.UpdateUrl = GetFileEditUrl(doc); } catch (Exception e) { Utilities.LogError("GDocsUploadHandler.SearchForFile", e); } finally { if (conn != null) { conn.disconnect(); } } return fal; } private String GetFileUploadUrl(Document fileSearchNode) { String fileUploadUrl = ""; NodeList linkNodes = fileSearchNode.getElementsByTagName("link"); for (int i = 0; i < linkNodes.getLength(); i++) { String rel = linkNodes.item(i).getAttributes().getNamedItem("rel").getNodeValue(); if (rel.equalsIgnoreCase("http://schemas.google.com/g/2005#resumable-create-media")) { fileUploadUrl = linkNodes.item(i).getAttributes().getNamedItem("href").getNodeValue(); } } return fileUploadUrl; } private String GetFileEditUrl(Document fileSearchNode) { String fileEditUrl = ""; NodeList linkNodes = fileSearchNode.getElementsByTagName("link"); for (int i = 0; i < linkNodes.getLength(); i++) { String rel = linkNodes.item(i).getAttributes().getNamedItem("rel").getNodeValue(); if (rel.equalsIgnoreCase("http://schemas.google.com/g/2005#resumable-edit-media")) { fileEditUrl = linkNodes.item(i).getAttributes().getNamedItem("href").getNodeValue(); } } return fileEditUrl; } private String CreateFolder() { String folderFeedUrl = ""; HttpURLConnection conn = null; String createFolderUrl = "https://docs.google.com/feeds/default/private/full"; String createXml = "<?xml version='1.0' encoding='UTF-8'?>\n" + "<entry xmlns=\"http://www.w3.org/2005/Atom\">\n" + " <category scheme=\"http://schemas.google.com/g/2005#kind\"\n" + " term=\"http://schemas.google.com/docs/2007#folder\"/>\n" + " <title>GPSLogger For Android</title>\n" + "</entry>"; try { URL url = new URL(createFolderUrl); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); AddCommonHeaders(conn); conn.setRequestProperty("Content-Type", "application/atom+xml"); conn.setUseCaches(false); conn.setDoInput(true); conn.setDoOutput(true); DataOutputStream wr = new DataOutputStream( conn.getOutputStream()); wr.writeBytes(createXml); wr.flush(); wr.close(); folderFeedUrl = GetFolderFeedUrlFromInputStream(conn.getInputStream()); } catch (Exception e) { Utilities.LogError("GDocsUploadHandler.CreateFolder", e); } finally { if (conn != null) { conn.disconnect(); } } return folderFeedUrl; } private String GetFolderFeedUrlFromInputStream(InputStream inputStream) { String folderFeedUrl = ""; Document createFolderDoc = Utilities.GetDocumentFromInputStream(inputStream); Node newFolderContentNode = createFolderDoc.getElementsByTagName("content").item(0); if (newFolderContentNode == null) { Utilities.LogInfo("Could not get collection info from response"); } else { folderFeedUrl = createFolderDoc.getElementsByTagName("content").item(0) .getAttributes().getNamedItem("src").getNodeValue(); } return folderFeedUrl; } private String SearchForGpsLoggerFolder() { String folderFeedUrl = ""; String searchUrl = "https://docs.google.com/feeds/default/private/full?title=GPSLogger+For+Android&showfolders=true"; HttpURLConnection conn = null; try { URL url = new URL(searchUrl); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); AddCommonHeaders(conn); folderFeedUrl = GetFolderFeedUrlFromInputStream(conn.getInputStream()); } catch (Exception e) { Utilities.LogError("GDocsUploadHandler.SearchForGpsLoggerFolder", e); } finally { if (conn != null) { conn.disconnect(); } } return folderFeedUrl; } /** * Adds headers commonly used when talking to Google APIs * * @param conn */ private void AddCommonHeaders(HttpURLConnection conn) { conn.addRequestProperty("client_id", GDocsHelper.GetClientID(ctx)); conn.addRequestProperty("client_secret", GDocsHelper.GetClientSecret(ctx)); conn.setRequestProperty("GData-Version", "3.0"); conn.setRequestProperty("User-Agent", "GPSLogger for Android"); conn.setRequestProperty("Authorization", "OAuth " + GDocsHelper.GetAuthToken(ctx)); } private class FileAccessLocations { public String CreateUrl; public String UpdateUrl; } } }