OK, so in the absence of any answers from others, I bit the bullet and wrote the wrapper myself.
Now I have the only shell that works for both Mac OS and iOS, which greatly simplifies the interface with Google Docs.
. , , , :
#define GOOGLE_DATA_CLIENT_ID @"<client id>.apps.googleusercontent.com"
#define GOOGLE_DATA_SECRET @"<google data secret>"
#define GOOGLE_DATA_USERNAME @"googleDocsUsername"
#define GOOGLE_DATA_PASSWORD @"googleDocsPassword"
, Google.
, NSUserDefaults . , , :
https://bitbucket.org/pkclsoft/gdatainterface
XCode, : Mac OS iOS. Ive , , . , , . SDK Google, . , SDK , .
, :
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE
#import "GDataDocs.h"
#import <UIKit/UIKit.h>
#else
#import "GData/GData.h"
#endif
@interface GDataInterfaceTypes
typedef void (^CompletionHandler)(BOOL successful);
typedef void (^UploadProgressHandler)(double min, double max, double value);
typedef void (^DownloadProgressHandler)(double min, double max, double value);
@end
@interface GDataInterface : NSObject {
#if TARGET_OS_IPHONE
UIViewController *rootController_;
#endif
GDataFeedDocList *mDocListFeed;
GDataServiceTicket *mDocListFetchTicket;
NSError *mDocListFetchError;
GDataFeedDocRevision *mRevisionFeed;
GDataServiceTicket *mRevisionFetchTicket;
NSError *mRevisionFetchError;
GDataEntryDocListMetadata *mMetadataEntry;
GDataServiceTicket *mUploadTicket;
id uploadWindow;
CompletionHandler uploadCompletionHandler;
NSString *username_;
NSString *password_;
}
typedef void (^RetrievalCompletionHandler)(GDataFeedDocList* results, BOOL successful);
typedef void (^DocumentDownloadCompletionHandler)(NSData* fileContents, BOOL successful);
- (id) initWithUsername:(NSString*)username andPassword:(NSString*)password;
+ (GDataInterface*) sharedInstance;
- (BOOL) isSignedIn;
- (void) signInOrOutWithCompletionHandler:(CompletionHandler)handler forWindow:(id)window;
- (void) retrieveDocumentListWithCompletionHandler:(RetrievalCompletionHandler)handler;
- (void) downloadURL:(NSURL*)url withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler;
- (void) downloadDocument:(GDataEntryDocBase*)document withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler;
- (void) uploadEntry:(GDataEntryDocBase*)docEntry asNewRevision:(BOOL)newRevision forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler;
- (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler;
- (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension;
- (void) setUsername:(NSString*)newUsername;
- (NSString*) username;
- (void) setPassword:(NSString*)newPassword;
- (NSString*) password;
- (NSString *)signedInUsername;
+ (GDataServiceGoogleDocs *)docsService;
@end
:
#import "GDataInterface.h"
#import "Util.h"
#if TARGET_OS_IPHONE
#import "GTMOAuth2ViewControllerTouch.h"
#import "GData.h"
#else
#import "GData/GTMOAuth2WindowController.h"
#endif
#define GOOGLE_DATA_CLIENT_ID @"<client id>.apps.googleusercontent.com"
#define GOOGLE_DATA_SECRET @"<google data secret>"
#define GOOGLE_DATA_USERNAME @"googleDocsUsername"
#define GOOGLE_DATA_PASSWORD @"googleDocsPassword"
@interface GDataInterface (PrivateMethods)
- (GDataServiceTicket *) uploadTicket;
- (void) setUploadTicket:(GDataServiceTicket *)ticket;
- (GDataFeedDocList *)docListFeed;
- (void)setDocListFeed:(GDataFeedDocList *)feed;
- (NSError *)docListFetchError;
- (void)setDocListFetchError:(NSError *)error;
- (GDataServiceTicket *)docListFetchTicket;
- (void)setDocListFetchTicket:(GDataServiceTicket *)ticket;
@end
@implementation GDataInterface
static NSString *const kKeychainItemName = @"GDataInterface: Google Docs";
- (id) initWithUsername:(NSString*)username andPassword:(NSString*)password {
self = [super init];
if (self != nil) {
username_ = [username retain];
password_ = [password retain];
[[GDataInterface docsService] setUserCredentialsWithUsername:username_ password:password_];
}
return self;
}
- (void) setUsername:(NSString*)newUsername {
username_ = [newUsername retain];
[[GDataInterface docsService] setUserCredentialsWithUsername:newUsername password:password_];
}
- (NSString*) username {
return username_;
}
- (void) setPassword:(NSString*)newPassword {
password_ = [newPassword retain];
[[GDataInterface docsService] setUserCredentialsWithUsername:username_ password:newPassword];
[Util setPassword:newPassword forKey:GOOGLE_DATA_PASSWORD];
}
- (NSString*) password {
return password_;
}
static GDataInterface *shared_instance_;
+ (GDataInterface*) sharedInstance {
if (shared_instance_ == nil) {
shared_instance_ = [[GDataInterface alloc] initWithUsername:[[NSUserDefaults standardUserDefaults] valueForKey:GOOGLE_DATA_USERNAME] andPassword:[Util getPassword:GOOGLE_DATA_PASSWORD]];
NSString *clientID = GOOGLE_DATA_CLIENT_ID;
NSString *clientSecret = GOOGLE_DATA_SECRET;
GTMOAuth2Authentication *auth;
#if TARGET_OS_IPHONE
auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:kKeychainItemName clientID:clientID clientSecret:clientSecret];
#else
auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName
clientID:clientID
clientSecret:clientSecret];
#endif
[[GDataInterface docsService] setAuthorizer:auth];
}
return shared_instance_;
}
- (NSString *)signedInUsername {
GTMOAuth2Authentication *auth = [[GDataInterface docsService] authorizer];
BOOL isSignedIn = auth.canAuthorize;
if (isSignedIn) {
return auth.userEmail;
} else {
return nil;
}
}
- (BOOL) isSignedIn {
return ([self signedInUsername] != nil);
}
- (void)runSigninThenInvokeHandler:(CompletionHandler)handler forWindow:(id)window {
NSString *clientID = GOOGLE_DATA_CLIENT_ID;
NSString *clientSecret = GOOGLE_DATA_SECRET;
NSString *scope = [GTMOAuth2Authentication scopeWithStrings:
[GDataServiceGoogleDocs authorizationScope],
[GDataServiceGoogleSpreadsheet authorizationScope],
nil];
#if TARGET_OS_IPHONE
NSAssert((window != nil), @"window must be a non-nil navigation controller");
GTMOAuth2ViewControllerTouch *viewController;
viewController = [GTMOAuth2ViewControllerTouch
controllerWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:kKeychainItemName
completionHandler:^(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error) {
[rootController_ dismissModalViewControllerAnimated:YES];
[rootController_ release];
rootController_ = nil;
if (error == nil) {
[[GDataInterface docsService] setAuthorizer:auth];
username_ = [self signedInUsername];
handler(YES);
} else {
NSLog(@"Authentication error: %@", error);
NSData *responseData = [[error userInfo] objectForKey:@"data"];
if ([responseData length] > 0) {
NSString *str = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] autorelease];
NSLog(@"%@", str);
}
handler(NO);
}
}];
NSString *html = @"<html><body bgcolor=silver><div align=center>Loading sign-in page...</div></body></html>";
viewController.initialHTMLString = html;
rootController_ = [(UIViewController*)window retain];
[rootController_ presentModalViewController:viewController animated:YES];
#else
NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]];
GTMOAuth2WindowController *windowController;
windowController = [GTMOAuth2WindowController controllerWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:kKeychainItemName
resourceBundle:frameworkBundle];
[windowController signInSheetModalForWindow:window
completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) {
if (error == nil) {
[[GDataInterface docsService] setAuthorizer:auth];
username_ = [auth userEmail];
handler(YES);
} else {
handler(NO);
}
}];
#endif
}
- (void) signInOrOutWithCompletionHandler:(CompletionHandler)handler forWindow:(id)window {
if (![self isSignedIn]) {
[self runSigninThenInvokeHandler:handler forWindow:window];
} else {
GDataServiceGoogleDocs *service = [GDataInterface docsService];
#if TARGET_OS_IPHONE
[GTMOAuth2ViewControllerTouch removeAuthFromKeychainForName:kKeychainItemName];
#else
[GTMOAuth2WindowController removeAuthFromKeychainForName:kKeychainItemName];
#endif
[service setAuthorizer:nil];
handler(YES);
}
}
- (void) retrieveDocumentListWithCompletionHandler:(RetrievalCompletionHandler)handler {
[self setDocListFeed:nil];
[self setDocListFetchError:nil];
[self setDocListFetchTicket:nil];
GDataServiceGoogleDocs *service = [GDataInterface docsService];
GDataServiceTicket *ticket;
NSURL *feedURL = [GDataServiceGoogleDocs docsFeedURL];
GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:feedURL];
[query setMaxResults:1000];
[query setShouldShowFolders:NO];
ticket = [service fetchFeedWithQuery:query
completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) {
[self setDocListFeed:(GDataFeedDocList *)feed];
[self setDocListFetchError:error];
[self setDocListFetchTicket:nil];
if (handler != nil) {
handler((GDataFeedDocList *)feed, (error == nil));
}
}];
[self setDocListFetchTicket:ticket];
}
- (void) downloadDocument:(GDataEntryDocBase*)document withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler {
NSURL *exportURL = [[document content] sourceURL];
if (exportURL != nil) {
GDataQuery *query = [GDataQuery queryWithFeedURL:exportURL];
[query addCustomParameterWithName:@"exportFormat"
value:@"txt"];
NSURL *downloadURL = [query URL];
NSURLRequest *request = [[GDataInterface docsService] requestForURL:downloadURL
ETag:nil
httpMethod:nil];
GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
[fetcher setAuthorizer:[[GDataInterface docsService] authorizer]];
__block double maxSize = 10240.0;
if (progressHandler != nil) {
[fetcher setReceivedDataBlock:^(NSData *dataReceivedSoFar) {
if ([[fetcher response] expectedContentLength] > 0) {
maxSize = [[fetcher response] expectedContentLength];
} else if ([dataReceivedSoFar length] > maxSize) {
maxSize += 5120.0;
}
progressHandler(0.0, maxSize, (double)[dataReceivedSoFar length]);
}];
}
[fetcher setCommentWithFormat:@"downloading \"%@\"", [[document title] stringValue]];
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
if (progressHandler != nil) {
progressHandler(0.0, (double)[data length], (double)[data length]);
}
if (error == nil) {
if (handler != nil) {
handler(data, YES);
}
} else {
if (handler != nil) {
handler(nil, NO);
}
}
}];
}
}
- (void) downloadURL:(NSURL*)url withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler {
NSURL *downloadURL = [url copy];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
__block double maxSize = 10240.0;
if (progressHandler != nil) {
[fetcher setReceivedDataBlock:^(NSData *dataReceivedSoFar) {
if ([[fetcher response] expectedContentLength] > 0) {
maxSize = [[fetcher response] expectedContentLength];
} else if ([dataReceivedSoFar length] > maxSize) {
maxSize += 5120.0;
}
progressHandler(0.0, maxSize, (double)[dataReceivedSoFar length]);
}];
}
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
if (progressHandler != nil) {
progressHandler(0.0, (double)[data length], (double)[data length]);
}
if (error == nil) {
if (handler != nil) {
handler(data, YES);
}
} else {
if (handler != nil) {
handler(nil, NO);
}
}
}];
[fetcher waitForCompletionWithTimeout:60.0];
if ([fetcher isFetching] == YES) {
[fetcher stopFetching];
if (handler != nil) {
handler(nil, NO);
}
}
}
#pragma mark Upload
- (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension {
struct MapEntry {
NSString *extension;
NSString *mimeType;
NSString *className;
};
static struct MapEntry sMap[] = {
{ @"csv", @"text/csv", @"GDataEntryStandardDoc" },
{ @"doc", @"application/msword", @"GDataEntryStandardDoc" },
{ @"docx", @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", @"GDataEntryStandardDoc" },
{ @"ods", @"application/vnd.oasis.opendocument.spreadsheet", @"GDataEntrySpreadsheetDoc" },
{ @"odt", @"application/vnd.oasis.opendocument.text", @"GDataEntryStandardDoc" },
{ @"pps", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" },
{ @"ppt", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" },
{ @"rtf", @"application/rtf", @"GDataEntryStandardDoc" },
{ @"sxw", @"application/vnd.sun.xml.writer", @"GDataEntryStandardDoc" },
{ @"txt", @"text/plain", @"GDataEntryStandardDoc" },
{ @"xls", @"application/vnd.ms-excel", @"GDataEntrySpreadsheetDoc" },
{ @"xlsx", @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @"GDataEntrySpreadsheetDoc" },
{ @"jpg", @"image/jpeg", @"GDataEntryStandardDoc" },
{ @"jpeg", @"image/jpeg", @"GDataEntryStandardDoc" },
{ @"png", @"image/png", @"GDataEntryStandardDoc" },
{ @"bmp", @"image/bmp", @"GDataEntryStandardDoc" },
{ @"gif", @"image/gif", @"GDataEntryStandardDoc" },
{ @"html", @"text/html", @"GDataEntryStandardDoc" },
{ @"htm", @"text/html", @"GDataEntryStandardDoc" },
{ @"tsv", @"text/tab-separated-values", @"GDataEntryStandardDoc" },
{ @"tab", @"text/tab-separated-values", @"GDataEntryStandardDoc" },
{ @"pdf", @"application/pdf", @"GDataEntryPDFDoc" },
{ nil, nil, nil }
};
NSString *lowerExtn = [extension lowercaseString];
for (int idx = 0; sMap[idx].extension != nil; idx++) {
if ([lowerExtn isEqual:sMap[idx].extension]) {
*mimeType = sMap[idx].mimeType;
*class = NSClassFromString(sMap[idx].className);
return;
}
}
*mimeType = nil;
*class = nil;
return;
}
- (void) uploadEntry:(GDataEntryDocBase*)docEntry asNewRevision:(BOOL)newRevision forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler {
uploadWindow = [window retain];
uploadCompletionHandler = [handler copy];
NSURL *uploadURL;
if (newRevision == YES) {
GDataQueryDocs *query = [GDataQueryDocs queryWithFeedURL:[[docEntry
uploadEditLink] URL]];
[query setShouldCreateNewRevision:YES];
uploadURL = [query URL];
} else {
uploadURL = [GDataServiceGoogleDocs docsUploadURL];
}
GDataServiceGoogleDocs *service = [GDataInterface docsService];
[service setServiceUploadProgressHandler:^(GDataServiceTicketBase *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) {
if (progressHandler != nil) {
progressHandler(0.0, (double)dataLength, (double)numberOfBytesRead);
}
}];
GDataServiceTicket *ticket;
if (newRevision == YES) {
ticket = [service fetchEntryByUpdatingEntry:docEntry
forEntryURL:uploadURL
delegate:self
didFinishSelector:@selector(uploadFileTicket:finishedWithEntry:error:)];
} else {
ticket = [service fetchEntryByInsertingEntry:docEntry
forFeedURL:uploadURL
delegate:self
didFinishSelector:@selector(uploadFileTicket:finishedWithEntry:error:)];
}
[ticket setUploadProgressHandler:^(GDataServiceTicketBase *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) {
if (progressHandler != nil) {
progressHandler(0.0, (double)dataLength, (double)numberOfBytesRead);
}
}];
[self setUploadTicket:ticket];
[service setServiceUploadProgressHandler:nil];
}
- (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler {
NSString *errorMsg = nil;
NSString *mimeType = nil;
Class entryClass = nil;
NSString *extn = [path pathExtension];
[self getMIMEType:&mimeType andEntryClass:&entryClass forExtension:extn];
if (!mimeType) {
mimeType = [GDataUtilities MIMETypeForFileAtPath:path
defaultMIMEType:nil];
entryClass = [GDataEntryFileDoc class];
}
if (mimeType && entryClass) {
GDataEntryDocBase *newEntry = [entryClass documentEnt