Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/ios/108.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
iOS Objective-C谷歌文档包装器_Objective C_Ios_Wrapper_Google Docs Api - Fatal编程技术网

iOS Objective-C谷歌文档包装器

iOS Objective-C谷歌文档包装器,objective-c,ios,wrapper,google-docs-api,Objective C,Ios,Wrapper,Google Docs Api,有没有人将谷歌文档集成到他们的iOS应用程序中?在阅读了示例代码之后,GoogleDocs的API比我预期的要复杂得多,而且这些示例都是MacOS。是的,有iOS支持,但是明显缺乏关于如何使用它的示例代码,文档也有点缺乏 我确实在网上找到了一个接口类,但它基于一个旧的、不推荐使用的GoogleDocsAPI版本,并且它不使用XCode 4.2编译 我想要的是一个相对直接的界面,它允许: 从google docs帐户登录/注销 获取该帐户中的文档列表(可选特定类型),可能具有浏览文件夹结构的能力

有没有人将谷歌文档集成到他们的iOS应用程序中?在阅读了示例代码之后,GoogleDocs的API比我预期的要复杂得多,而且这些示例都是MacOS。是的,有iOS支持,但是明显缺乏关于如何使用它的示例代码,文档也有点缺乏

我确实在网上找到了一个接口类,但它基于一个旧的、不推荐使用的GoogleDocsAPI版本,并且它不使用XCode 4.2编译

我想要的是一个相对直接的界面,它允许:

  • 从google docs帐户登录/注销
  • 获取该帐户中的文档列表(可选特定类型),可能具有浏览文件夹结构的能力
  • 能够将特定文档下载到本地存储
  • 能够将特定文档上传到谷歌文档
  • 我已经开始编写这样一个接口,但到目前为止,它比我想象的要复杂得多。如果任何人有任何建议,或样品,他们可以告诉我,我真的很感激

    我的偏好是包装器与操作系统无关;这意味着我希望能够在MacOS和iOS中使用相同的界面。同样,这是我开始写的,但我忍不住觉得我必须在这里重新发明轮子


    谢谢

    好的,在没有其他人回答的情况下,我咬紧牙关,自己写了一个包装纸

    我现在有了一个适用于Mac OS和iOS的包装器,大大简化了与Google文档的接口

    下面是实际接口的所有代码。我应该指出,这个类作为一个单例,您需要通过更新行为每个项目稍微定制它:

    #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_数据_客户端_ID@“。apps.googleusercontent.com”
    #定义GOOGLE_DATA_SECRET@“”
    #定义GOOGLE_数据_用户名@“googleDocsUsername”
    #定义GOOGLE_数据_密码@“googleDocsPassword”
    
    使用从Google获得的适当值

    我还指出,该类通过NSUserDefaults存储密码,并使用一个单独的实用程序类以加密的方式进行存储。我在bitbucket中创建了一个存储库,位于:

    它包含一个完整的XCode项目,构建两个目标,一个用于Mac OS,一个用于iOS。我在app store中的应用程序中使用了这两种方法,并取得了很好的效果。这可能是,虽然这个项目为我建设,你必须调整它为您的目的。该项目包含一套完整的GoogleSDK,它使用我的代码构建和运行。我加入它是为了尝试降低任何人获取它时与新版本SDK不兼容的风险

    以下是当前的接口规范:

    //
    //  GDataInterface.h
    //  GDataInterface
    //
    //  Some of the code in this class is from the original GData sample code, but it has been
    //  enhanced somewhat and made to work on both iOS and MacOS transparently.
    //
    //  Created by Peter Easdown on 19/12/11.
    //  Copyright (c) 2011 PKCLsoft. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #if TARGET_OS_IPHONE
    #import "GDataDocs.h"
    #import <UIKit/UIKit.h>
    #else
    #import "GData/GData.h"
    #endif
    
    @interface GDataInterfaceTypes
    
    // This handler is used by methods that have no explicit result.  The boolean value indicates
    // the success or failure of the the methods action.
    //
    typedef void (^CompletionHandler)(BOOL successful);
    
    // This handler is called to update a progress indicator as a file is uploaded.
    //
    typedef void (^UploadProgressHandler)(double min, double max, double value);
    
    // This handler is called to update a progress indicator as a file is downloaded.
    //
    typedef void (^DownloadProgressHandler)(double min, double max, double value);
    
    @end
    
    @interface GDataInterface : NSObject {
    
    #if TARGET_OS_IPHONE
        // Needed so that when authenticating under iOS, we can push the google authentication
        // view, and later pop it.
        //
        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_;
    
    }
    
    // This handler is used when a list of documents has been requested.  The results parameter
    // will be nil if the request failed.  If successful, then it will contain an array of 
    // GDataEntryDocBase objects.
    //
    typedef void (^RetrievalCompletionHandler)(GDataFeedDocList* results, BOOL successful);
    
    // This handler is used when a document has been downloaded.  If something prevented the 
    // download from succeeding, then error parameter will be non-nil.
    //
    typedef void (^DocumentDownloadCompletionHandler)(NSData* fileContents, BOOL successful);
    
    // Initializer that provides the username and password.
    //
    - (id) initWithUsername:(NSString*)username andPassword:(NSString*)password;
    
    // Returns the shared instance of the class.  There will only ever be a single instance
    // of this class.
    //
    + (GDataInterface*) sharedInstance;
    
    // Returns YES if currently signed in.
    //
    - (BOOL) isSignedIn;
    
    // Signs in or out depending on current state, and executes the options completion handler
    // block.  The window parameter is used to specify the root viewController object used when
    // displaying login windows via GData, or error dialogs.
    //
    - (void) signInOrOutWithCompletionHandler:(CompletionHandler)handler forWindow:(id)window;
    
    // Will retrieve a list of documents using the cached connection, and call the specified
    // handler block, providing the list of documents, and a success/fail indication.
    //
    - (void) retrieveDocumentListWithCompletionHandler:(RetrievalCompletionHandler)handler;
    
    // Will download the file at the specified URL.  This is not Google Docs specific and will work
    // for any URL.  Be careful not to try and retrieve large files and the result is stored
    // in memory.
    //
    - (void) downloadURL:(NSURL*)url withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler;
    
    // Will download the specified google docs document.
    //
    - (void) downloadDocument:(GDataEntryDocBase*)document withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler;
    
    // Uploads the document entry, optionally updating it with a new revision.
    //
    - (void) uploadEntry:(GDataEntryDocBase*)docEntry asNewRevision:(BOOL)newRevision forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler;
    
    // Uploads the specified file to the authenticated google docs account.
    //
    - (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler;
    
    // More for internal use than anything else.  Used to determine the mime type based on the google docs class
    // and/or file extension.
    //
    - (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension;
    
    // Getter and Setter for username,
    //
    - (void) setUsername:(NSString*)newUsername;
    - (NSString*) username;
    
    // Getter and Setter for password.  The password will be encrypted before storing it in user preferances.
    //
    - (void) setPassword:(NSString*)newPassword;
    - (NSString*) password;
    
    // Returns the username that google is given for signing in.
    //
    - (NSString *)signedInUsername;
    
    // Returns a static instance of the docs service.
    //
    + (GDataServiceGoogleDocs *)docsService;
    
    @end
    
    //
    //GDataInterface.h
    //GDataInterface
    //
    //这个类中的一些代码来自原始的GData示例代码,但它已经被删除了
    //在某种程度上进行了增强,使其能够透明地在iOS和MacOS上工作。
    //
    //由Peter Easdown于2011年12月19日创作。
    //版权所有(c)2011 PKCLsoft。版权所有。
    //
    #进口
    #如果目标是IPHONE
    #导入“GDataDocs.h”
    #进口
    #否则
    #导入“GData/GData.h”
    #恩迪夫
    @接口接口类型
    //此处理程序由没有显式结果的方法使用。布尔值表示
    //行动方法的成功或失败。
    //
    typedef void(^CompletionHandler)(BOOL成功);
    //此处理程序被调用以在上载文件时更新进度指示器。
    //
    typedef void(^UploadProgressHandler)(双最小值、双最大值、双值);
    //调用此处理程序以在下载文件时更新进度指示器。
    //
    typedef void(^DownloadProgressHandler)(双最小值、双最大值、双值);
    @结束
    @接口接口:NSObject{
    #如果目标是IPHONE
    //需要这样,当在iOS下进行身份验证时,我们可以推送google身份验证
    //查看,然后将其弹出。
    //
    UIViewController*根控制器389;;
    #恩迪夫
    GDataFeedDocList*mDocListFeed;
    GdatServiceticket*mDocListFetchTicket;
    n错误*mDocListFetchError;
    GDataFeedDocRevision*mRevisionFeed;
    GdatServiceticket*mRevisionFetchTicket;
    n错误*mRevisionFetchError;
    GDataEntryDocListMetadata*mMetadataEntry;
    GdatServiceticket*mUploadTicket;
    id上传窗口;
    CompletionHandler上载CompletionHandler;
    NSString*用户名389;;
    NSString*密码;
    }
    //此处理程序在请求文档列表时使用。结果参数
    //如果请求失败,则将为零。如果成功,那么它将包含一个
    //GDataEntryDocBase对象。
    //
    typedef void(^RetrievalCompletionHandler)(GDataFeedDocList*结果,BOOL成功);
    //此处理程序在下载文档时使用。如果有什么事情阻止了
    //从成功下载,则错误参数将为非零。
    //
    typedef void(^DocumentDownloadCompletionHandler)(NSData*fileContents,BOOL成功);
    //提供用户名和密码的初始值设定项。
    //
    -(id)initWithUsername:(NSString*)用户名和密码:(NSString*)密码;
    //返回类的共享实例。只有一个实例
    //这个班的学生。
    //
    +(GDataInterface*)共享状态;
    //如果当前已登录,则返回YES。
    //
    -(BOOL)伊西涅丁;
    //根据当前状态登录或注销,并执行选项完成处理程序
    //街区。window参数用于指定在以下情况下使用的根viewController对象:
    //通过GData或错误对话框显示登录窗口。
    //
    -(void)signinoroweithCompletionHandler:(CompletionHandler)窗口处理程序:(id)窗口;
    //将使用缓存连接检索文档列表,并调用指定的
    //handler块,提供文档列表和成功/失败指示。
    //
    -(作废)检索到的文档列表
    
    //
    //  GDataInterface.m
    //  GDataInterface
    //
    //  Created by Peter Easdown on 19/12/11.
    //  Copyright (c) 2011 PKCLsoft. All rights reserved.
    //
    
    #import "GDataInterface.h"
    #import "Util.h"
    #if TARGET_OS_IPHONE
    #import "GTMOAuth2ViewControllerTouch.h"
    #import "GData.h"
    #else
    #import "GData/GTMOAuth2WindowController.h"
    //#import "GDataServiceGoogleSpreadsheet.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";
    
    // Initializer that provides the username and password.
    //
    - (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_;
    
    // Returns the shared instance of the class.  There will only ever be a single instance
    // of this class.
    //
    + (GDataInterface*) sharedInstance {
        if (shared_instance_ == nil) {
            shared_instance_ = [[GDataInterface alloc] initWithUsername:[[NSUserDefaults standardUserDefaults] valueForKey:GOOGLE_DATA_USERNAME] andPassword:[Util getPassword:GOOGLE_DATA_PASSWORD]];
    
            // Load the OAuth token from the keychain, if it was previously saved
            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 {
        // Get the email address of the signed-in user
        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 {
        // Applications should have client ID and client secret strings
        // hardcoded into the source, but the sample application asks the
        // developer for the strings
        NSString *clientID = GOOGLE_DATA_CLIENT_ID;
        NSString *clientSecret = GOOGLE_DATA_SECRET;
    
        // Show the OAuth 2 sign-in controller
        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;
    
                              // callback
                              if (error == nil) {
                                  [[GDataInterface docsService] setAuthorizer:auth];
    
                                  username_ = [self signedInUsername];
    
                                  handler(YES);
                              } else {
                                  NSLog(@"Authentication error: %@", error);
                                  NSData *responseData = [[error userInfo] objectForKey:@"data"]; // kGTMHTTPFetcherStatusDataKey
                                  if ([responseData length] > 0) {
                                      // show the body of the server's authentication failure response
                                      NSString *str = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] autorelease];
                                      NSLog(@"%@", str);
                                  }
                                  handler(NO);
                              }
                          }];
    
        // Optional: display some html briefly before the sign-in page loads
        NSString *html = @"<html><body bgcolor=silver><div align=center>Loading sign-in page...</div></body></html>";
        viewController.initialHTMLString = html;
    
        // For iOS, window is a navigation controller.
        //
        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) {
                                      // callback
                                      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]) {
            // Sign in
            [self runSigninThenInvokeHandler:handler forWindow:window];
        } else {
            // Sign out
            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;
    
        // Fetching a feed gives us 25 responses by default.  We need to use
        // the feed's "next" link to get any more responses.  If we want more than 25
        // at a time, instead of calling fetchDocsFeedWithURL, we can create a
        // GDataQueryDocs object, as shown here.
    
        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) {
                               // callback
                               [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 {
    
        // the content src attribute is used for downloading
        NSURL *exportURL = [[document content] sourceURL];
    
        if (exportURL != nil) {
            GDataQuery *query = [GDataQuery queryWithFeedURL:exportURL];
            [query addCustomParameterWithName:@"exportFormat"
                                        value:@"txt"];
            NSURL *downloadURL = [query URL];
            // Read the document's contents asynchronously from the network
    
            // requestForURL:ETag:httpMethod: sets the user agent header of the
            // request and, when using ClientLogin, adds the authorization header
            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) {
                    // Update the progress handler with a "complete" progress.
                    //
                    progressHandler(0.0, (double)[data length], (double)[data length]);
                }
    
                // callback
                if (error == nil) {
                    // Successfully downloaded the document
                    //                
                    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];
        // Read the document's contents asynchronously from the network
    
        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]);
            }
    
            // callback
            if (error == nil) {
                // Successfully downloaded the document
                //                
                if (handler != nil) {
                    handler(data, YES);
                }
            } else {
                if (handler != nil) {
                    handler(nil, NO);
                }
            }
        }];
    
        // Block, waiting for 60 seconds for the download.
        //
        [fetcher waitForCompletionWithTimeout:60.0];
    
        if ([fetcher isFetching] == YES) {
            // OK, so this looks like we've timed out waiting for the download to complete.  Cancel the 
            // fetch.
            //
            [fetcher stopFetching];
    
            if (handler != nil) {
                handler(nil, NO);
            }
        }
    }
    
    #pragma mark Upload
    
    - (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension {
    
        // Mac OS X's UTI database doesn't know MIME types for .doc and .xls
        // so GDataEntryBase's MIMETypeForFileAtPath method isn't helpful here
    
        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];
        }
    
        // make service tickets call back into our upload progress selector
        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);
            }
        }];
    
        // insert the entry into the docList feed
        //
        // to update (replace) an existing entry by uploading a new file,
        // use the fetchEntryByUpdatingEntry:forEntryURL: with the URL from
        // the entry's uploadEditLink
        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) {
            // progress callback
            if (progressHandler != nil) {
                progressHandler(0.0, (double)dataLength, (double)numberOfBytesRead);
            }
        }];
    
        // we turned automatic retry on when we allocated the service, but we
        // could also turn it on just for this ticket
    
        [self setUploadTicket:ticket];
        [service setServiceUploadProgressHandler:nil];
    }
    
    - (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler {
    
        NSString *errorMsg = nil;
    
        // make a new entry for the file
    
        NSString *mimeType = nil;
        Class entryClass = nil;
    
        NSString *extn = [path pathExtension];
        [self getMIMEType:&mimeType andEntryClass:&entryClass forExtension:extn];
    
        if (!mimeType) {
            // for other file types, see if we can get the type from the Mac OS
            // and use a generic file document entry class
            mimeType = [GDataUtilities MIMETypeForFileAtPath:path
                                             defaultMIMEType:nil];
            entryClass = [GDataEntryFileDoc class];
        }
    
        if (mimeType && entryClass) {
    
            GDataEntryDocBase *newEntry = [entryClass documentEnt