Оболочка iOS Objective-C для Документов Google - PullRequest
6 голосов
/ 30 декабря 2011

Кто-нибудь интегрировал Google Docs в свое приложение для iOS?Изучив код примера, API для Google Docs гораздо сложнее, чем я ожидал, и все примеры - MacOS.Да, есть поддержка iOS, но явно отсутствует пример кода о том, как его использовать, и документации немного не хватает.

Я нашел класс интерфейса в Интернете, но он был основан настарая устаревшая версия API Google Docs, и она не совместима с XCode 4.2.

Мне нужен относительно простой интерфейс, который позволяет:

  1. Ведение журналавход / выход из учетной записи Google Docs.
  2. Получение списка документов в этой учетной записи (необязательно определенного типа), возможно, с возможностью навигации по структуре папок.
  3. Возможностьзагрузить определенный документ в локальное хранилище.
  4. Возможность загрузить определенный документ в Google Docs.

Я начал писать такой интерфейс, но пока он более сложныйчем я позволил.Если у кого-то есть какие-либо предложения или примеры, на которые они могут направить меня, я буду очень признателен.

Я предпочитаю, чтобы оболочка была нейтральной к ОС;Я хочу иметь возможность использовать один и тот же интерфейс в MacOS и iOS.Опять же, это то, что я начал писать, но я не могу избавиться от ощущения, что я, должно быть, заново изобретаю колесо.

Спасибо

1 Ответ

5 голосов
/ 15 января 2012

ОК, поэтому, в отсутствие каких-либо ответов от других, я укусил пулю и сам написал обертку.

Теперь у меня есть одна оболочка, которая работает как для Mac OS, так и для iOS, что значительно упрощает интерфейс с 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 и использует отдельный служебный класс, чтобы сделать это в зашифрованном виде.Вместо того, чтобы засорять этот ответ здесь всем этим дополнительным кодом, я создал хранилище в bitbucket по адресу:

https://bitbucket.org/pkclsoft/gdatainterface

, которое содержит весь проект XCode, который создает две цели, однудля Mac OS и один для iOS.Я использовал оба этих приложения в приложении, которое теперь находится в магазине приложений, с отличными результатами.Возможно, что пока этот проект создается для меня, вы должны настроить его для своих целей.Проект содержит полный набор Google SDK, который собирается и работает с моим кодом.Я включил его, чтобы уменьшить риск несовместимости с более новыми версиями 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.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
...