Sunday, August 4, 2013

Google APIs SpreadSheetService OAuth2

How to Deal with Google SpreadSheet API and OAuth2

This is my first blog post please bear with me. I want to try and help others to deal with the god blessed Google APIs. As many of you may already known before finding this post, Google documentation is lacking Android examples. This post is good as of 20130805, I do not recommend using this example after 5 month of mentioned date. I have noticed a lot of post are old and out of date this is why I have forewarned you about the code below. Nonetheless all old post have helped me understand how to get it to work for me. A big THANK YOU to all!

On Android Google recommends the use of AccountManager in some pages and in some others it does not. As far as I can tell AccountManager is the best method of dealing with accounts and making request to OAuth2. After you are all setup, you run into issues with Spreadsheetservice and the end point URI. This is the reason I have posted my solution to my problem.

Please download your libraries and import as posted on the code below for this to work. Do not use the Google Plugins to install Drive APIs and Spreadsheets, in fact remove them.

Libraries required, they can all be found google-oauth-java-client,  Google OAuth2 API and gdata-java-client:

gdata-spreadsheet-3.0.jar
gdata-spreadsheet-meta-3.0.jar
google-api-client-android-1.15.0-rc.jar
google-api-services-oauth2-v2-rev40-1.15.0-rc-javadoc.jar
google-api-services-oauth2-v2-rev40-1.15.0-rc-sources.jar
google-api-services-oauth2-v2-rev40-1.15.0-rc.jar
google-http-client-1.15.0-rc.jar
google-http-client-android-1.15.0-rc.jar
google-oauth-client-1.15.0-rc.jar
gson-2.1.jar
jackson-core-2.1.3.jar
jackson-core-asl-1.9.11.jar
jsr305-1.3.9.jar
protobuf-java-2.4.1.jar
google-play-services.jar (use instructions below code to install)
gdata-core-1.0.jar
guava-11.0.2.jar
gdata-client-meta-1.0.jar

On your manifest file do not forget

<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>

There is more instructions and references after the code. Please comment if this is helpful, requires updating or if I just plain need psychological help.

 package com.example.dailyexpense;  
 /**  
  * Developed with the help of too many to mention, but to all THANKS!  
  * By: Antonio M Vazquez  
  * Date last: 20130805  
  * This program read all spreadsheets available in google drive. This program will add rows to specific worksheets in  
  * Google drive worksheets.  
  */  
 import java.io.IOException;  
 import java.util.ArrayList;  
 import java.util.List;  
 import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;  
 import com.google.gdata.client.spreadsheet.FeedURLFactory;  
 import com.google.gdata.client.spreadsheet.SpreadsheetService;  
 import com.google.gdata.data.spreadsheet.*;  
 import com.google.gdata.util.AuthenticationException;  
 import com.google.gdata.util.ServiceException;  
 import android.os.Bundle;  
 import android.accounts.AccountManager;  
 import android.accounts.AccountManagerCallback;  
 import android.accounts.AccountManagerFuture;  
 import android.accounts.AuthenticatorException;  
 import android.accounts.OperationCanceledException;  
 import android.app.Activity;  
 import android.content.Intent;  
 import android.view.Menu;  
 import android.widget.EditText;  
 /*  
  * Required google-play-services to allow account picker to lunch.  
  * Additional libraries install by hand. Google Plugins do not play well with each other.  
  */  
 public class MainActivity extends Activity {  
      GoogleAccountCredential GACredential;       
      EditText txtDump; //I like to see what I do so created a debugger window on the device  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_main);  
           //created debuger viewer  
           txtDump = (EditText) findViewById(R.id.etDump);  
           //Set up the scopes needed by GSSAPI (Giuseppi)  
           final List<String> SCOPES = new ArrayList<String>();   
           SCOPES.add("https://spreadsheets.google.com/feeds/");  
           SCOPES.add("https://docs.google.com/feeds/");  
           GACredential = GoogleAccountCredential.usingOAuth2(this, SCOPES);  
           startActivityForResult(GACredential.newChooseAccountIntent(), 1);  
      }  
      @Override  
      protected void onActivityResult(final int requestCode, final int reultCode, final Intent data){  
           AccountManager am = AccountManager.get(this);  
           //Received data with user account name after default account picker was used.  
           //basically email address  
           String accountname = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);  
           txtDump = (EditText) findViewById(R.id.etDump);  
           //Set selected account name  
           GACredential.setSelectedAccountName(accountname);  
           //Display what I have so far  
           txtDump.append(accountname+"\n");  
           txtDump.append("credential.scope - " + GACredential.getScope()+"\n");  
           //To get auth token it must be done in background process as UI may frezze  
           //The call back is handle within theis method and forwaded to useSprtexteadsheetAPI  
           am.getAuthToken(GACredential.getSelectedAccount(), GACredential.getScope(),  
                     null, this, new AccountManagerCallback<Bundle>() {  
                @Override  
                public void run(AccountManagerFuture<Bundle> future) {  
                     // Callback received information on token request for later to use  
                     try {  
                          Bundle bundleResults;  
                          bundleResults = future.getResult();  
                          //Just testing and displaying bundle and future information  
                          txtDump.append("\n Account Manager Callback \n");  
                          txtDump.append("KEY_ACCOUNT_NAME - " + future.getResult().getString(AccountManager.KEY_ACCOUNT_NAME) +"\n");  
                          String token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);  
                          txtDump.append("KEY_AUTHTOKEN - " + token +"\n");  
                          txtDump.append("KEY_INTENT - " + future.getResult().getString(AccountManager.KEY_INTENT) +"\n\n");  
                          //Send results in a bundle to the API accessor  
                          useSpreadsheetAPI(bundleResults);  
                     } catch (OperationCanceledException e) {  
                          // TODO Auto-generated catch block  
                          txtDump.append("OperationCanceledException \n\n");  
                          e.printStackTrace();  
                     } catch (AuthenticatorException e) {  
                          // TODO Auto-generated catch block  
                          txtDump.append("AuthenticatorException \n\n");  
                          e.printStackTrace();  
                     } catch (IOException e) {  
                          // TODO Auto-generated catch block  
                          txtDump.append("IOException \n\n");  
                          e.printStackTrace();  
                     }  

                }  
           }, null);  
      }  
      public void useSpreadsheetAPI(Bundle myBundle) {  
           txtDump = (EditText) findViewById(R.id.etDump);  
           //Instantiated here to invalidate token on service error  
           AccountManager am = AccountManager.get(this);  
           //create the service with the app name  
           SpreadsheetService ssservice = new SpreadsheetService("dailyexpense");  
           String myToken = myBundle.getString(AccountManager.KEY_AUTHTOKEN);  
           /*  
            * Assign my token to the service. Accountmanager above is able to get this token to access Giusepi without  
            * Client ID or key for the registered app. What is interesting is that there is vague refenrences about this.  
            */  
           ssservice.setAuthSubToken(myToken);  
           //Playing with values to test what is received from the callback future etc.  
           txtDump.append("\n\n Entered useSpreadsheetAPI \n");//The code below crashes!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!  
           txtDump.append("AUTHTOKEN -" + myToken + "\n");  
           txtDump.append("KEY_ACCOUNT_TYPE -" + myBundle.getString(AccountManager.KEY_ACCOUNT_TYPE) + "\n");  
           txtDump.append("KEY_AUTHENTICATOR_TYPES -" + myBundle.getString(AccountManager.KEY_AUTHENTICATOR_TYPES) + "\n");  
           txtDump.append("KEY_INTENT - " + myBundle.getString(AccountManager.KEY_INTENT) + "\n");  
           try {  
                /*  
                 * Painfully attempted to use URL method to create my "URL SPREADSHEET_FEED_URL =   
                 * new URL("https://spreadsheets.google.com/feeds/spreadsheets/private/full");"  
                 * The above does not work for me I get 400 error URI not properly formated  
                 * This FeedURLFactory.getDefault().getSpreadsheetsFeedUrl() gets the valid service alias value   
                 * or URI... God bless google...  
                 *   
                 */  
                SpreadsheetFeed feed = ssservice.getFeed(FeedURLFactory.getDefault().getSpreadsheetsFeedUrl(), SpreadsheetFeed.class);  
                //View what FeedURLFactory.getDefault().getSpreadsheetsFeedUrl() provides  
                txtDump.append("URL - " + FeedURLFactory.getDefault().getSpreadsheetsFeedUrl() + "\n");  
                txtDump.append("Feed plain- " + feed.getTitle().getPlainText() + "\n");       
                txtDump.append("\n Listing all Spreadhssets and Keys \n");       
                //Creating the list of spreasheets in GDrive  
                List<SpreadsheetEntry> spreadsheets = feed.getEntries();  
                //parsing trough the feed entries  
                for (int i = 0; i < spreadsheets.size(); i++){  
                SpreadsheetEntry entry = (SpreadsheetEntry) spreadsheets.get(i);  
                txtDump.append(entry.getTitle().getPlainText() + "\n\t" + entry.getKey() +"\n");  
                }  
                //View what information is in Feed  
                txtDump.append("Feed - " + feed.toString() + "\n");  
           } catch (IOException e) {  
                // TODO Auto-generated catch block  
                txtDump.append("IOExeption \n");  
                e.printStackTrace();  
           } catch (ServiceException e) {  
                // TODO Auto-generated catch block  
                txtDump.append("ServiceExeption - " + e.getHttpErrorCodeOverride() + "\n");  
                //If service error is 400 or 401 it resets the token, you must restart app to use new token  
                am.invalidateAuthToken(myBundle.getString(AccountManager.KEY_ACCOUNT_TYPE), myBundle.getString(AccountManager.KEY_AUTHTOKEN));  
                e.printStackTrace();  
           }  
      }  
      @Override  
      public boolean onCreateOptionsMenu(Menu menu) {  
           // Inflate the menu; this adds items to the action bar if it is present.  
           getMenuInflater().inflate(R.menu.main, menu);  
           return true;  
      }  
 }  

Additional Instructions and References

I have developed the code above on Eclipse Kepler (Fedora 19). If you are using this setup follow links below on how to install and prepare Eclipse of Fedora 19. Setup Android Development Fedora-EclipseEclipse ADT Plugin Additional references. Install Google play services, and Google Plugins for Eclipse ( this did not really helped me since I added all libraries manually, in fact caused more issues). This last plugin makes all google APIs readily available but the libraries have a tendency to be duplicated and cause errors.

Make sure you register your app using Google console. Console, you do not need to enable any services in console.

Use Generate Key fingerprint SHA1 instruction to create your SHA1 ONLY. If this is your first project on Eclipse you may not have a ~/.android/debug.keystore signing certificate fingerprint (SHA1) because you have not run debugger on Eclipse. Create the easy hello world example and run in debug mode to have the debug.keystore created and then run keytool as per Generate Key fingerprint SHA1.