GBA003/External/Harmony/Backends/Drive/Google/GoogleAPI/Examples/DriveSample/DriveSampleWindowController.m
2024-05-30 10:22:15 +08:00

1163 lines
39 KiB
Objective-C

/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// DriveSampleWindowController.m
//
#import "DriveSampleWindowController.h"
#import <AppAuth/AppAuth.h>
#import <GTMAppAuth/GTMAppAuth.h>
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
#import <GTMSessionFetcher/GTMSessionFetcherLogging.h>
#import <GoogleAPIClientForREST/GTLRUtilities.h>
// Segmented control indices.
enum {
kRevisionsSegment = 0,
kPermissionsSegment,
kChildrenSegment,
kParentsSegment
};
// This is the URL shown users after completing the OAuth flow. This is an information page only and
// is not part of the authorization protocol. You can replace it with any URL you like.
// We recommend at a minimum that the page displayed instructs users to return to the app.
static NSString *const kSuccessURLString = @"http://openid.github.io/AppAuth-iOS/redirect/";
// Keychain item name for saving the user's authentication information.
NSString *const kGTMAppAuthKeychainItemName = @"DriveSample: Google Drive. GTMAppAuth.";
@interface DriveSampleWindowController ()
@property (nonatomic, readonly) GTLRDriveService *driveService;
@end
@implementation DriveSampleWindowController {
GTLRDrive_FileList *_fileList;
GTLRServiceTicket *_fileListTicket;
NSError *_fileListFetchError;
GTLRServiceTicket *_editFileListTicket;
GTLRServiceTicket *_uploadFileTicket;
// Details
GTLRDrive_RevisionList *_revisionList;
NSError *_revisionListFetchError;
GTLRDrive_PermissionList *_permissionList;
NSError *_permissionListFetchError;
GTLRDrive_FileList *_childList;
NSError *_childListFetchError;
NSArray *_parentsList;
NSError *_parentsListFetchError;
GTLRServiceTicket *_detailsTicket;
NSError *_detailsFetchError;
OIDRedirectHTTPHandler *_redirectHTTPHandler;
}
+ (DriveSampleWindowController *)sharedWindowController {
static DriveSampleWindowController* gWindowController = nil;
if (!gWindowController) {
gWindowController = [[DriveSampleWindowController alloc] init];
}
return gWindowController;
}
- (id)init {
return [self initWithWindowNibName:@"DriveSampleWindow"];
}
- (void)awakeFromNib {
// Attempts to deserialize authorization from keychain in GTMAppAuth format.
id<GTMFetcherAuthorizationProtocol> authorization =
[GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kGTMAppAuthKeychainItemName];
self.driveService.authorizer = authorization;
// Set the result text fields to have a distinctive color and mono-spaced font.
_fileListResultTextField.textColor = [NSColor darkGrayColor];
_detailResultTextField.textColor = [NSColor darkGrayColor];
NSFont *resultTextFont = [NSFont fontWithName:@"Monaco" size:9];
_fileListResultTextField.font = resultTextFont;
_detailResultTextField.font = resultTextFont;
[self updateUI];
}
#pragma mark -
- (NSString *)signedInUsername {
// Get the email address of the signed-in user.
id<GTMFetcherAuthorizationProtocol> auth = self.driveService.authorizer;
BOOL isSignedIn = auth.canAuthorize;
if (isSignedIn) {
return auth.userEmail;
} else {
return nil;
}
}
- (BOOL)isSignedIn {
NSString *name = [self signedInUsername];
return (name != nil);
}
#pragma mark IBActions
- (IBAction)signInClicked:(id)sender {
if (![self isSignedIn]) {
// Sign in
[self runSigninThenHandler:^{
[self updateUI];
}];
} else {
// Sign out
GTLRDriveService *service = self.driveService;
[GTMAppAuthFetcherAuthorization
removeAuthorizationFromKeychainForName:kGTMAppAuthKeychainItemName];
service.authorizer = nil;
[self updateUI];
}
}
- (IBAction)segmentedControlClicked:(id)sender {
[self updateUI];
}
- (IBAction)getFileList:(id)sender {
if (![self isSignedIn]) {
[self runSigninThenHandler:^{
[self fetchFileList];
}];
} else {
[self fetchFileList];
}
}
- (IBAction)cancelFileListFetch:(id)sender {
[_fileListTicket cancelTicket];
_fileListTicket = nil;
[_editFileListTicket cancelTicket];
_editFileListTicket = nil;
[self updateUI];
}
- (IBAction)viewClicked:(id)sender {
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *viewURLString = selectedFile.webViewLink;
if (viewURLString.length > 0) {
NSURL *url = [NSURL URLWithString:viewURLString];
[[NSWorkspace sharedWorkspace] openURL:url];
}
}
- (IBAction)duplicateClicked:(id)sender {
[self duplicateSelectedFile];
}
- (IBAction)trashClicked:(id)sender {
[self changeTrashStateForSelectedFile];
}
- (IBAction)deleteClicked:(id)sender {
GTLRDrive_File *file = [self selectedFileListEntry];
NSString *title = file.name;
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = [NSString stringWithFormat:@"Delete \"%@\"?", title];
[alert addButtonWithTitle:@"Delete"];
[alert addButtonWithTitle:@"Cancel"];
[alert beginSheetModalForWindow:[self window]
completionHandler:^(NSModalResponse returnCode) {
if (returnCode == NSAlertFirstButtonReturn) {
[self deleteSelectedFile];
}
}];
}
- (IBAction)downloadClicked:(id)sender {
[self showDownloadSavePanelExportingToPDF:NO];
}
- (IBAction)exportAsPDFClicked:(id)sender {
[self showDownloadSavePanelExportingToPDF:YES];
}
- (void)showDownloadSavePanelExportingToPDF:(BOOL)isExportingToPDF {
GTLRDrive_File *file = [self selectedFileListEntry];
NSString *suggestedName = file.originalFilename;
if (suggestedName.length == 0) {
suggestedName = file.name;
}
NSString *likelyExtension;
if (isExportingToPDF) {
likelyExtension = @"pdf";
} else {
// If the Mac can tell us the likely extension for the file based on the content type,
// add that extension.
//
// Note that MIME types of Google Drive native formats won't be known to the Mac.
// See https://developers.google.com/drive/v3/web/mime-types
likelyExtension = [self extensionForMIMEType:file.mimeType];
}
if (likelyExtension.length > 0 && ![suggestedName.pathExtension isEqual:likelyExtension]) {
suggestedName = [suggestedName stringByAppendingPathExtension:likelyExtension];
}
NSSavePanel *savePanel = [NSSavePanel savePanel];
savePanel.extensionHidden = NO;
savePanel.nameFieldStringValue = suggestedName;
[savePanel beginSheetModalForWindow:[self window]
completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
[self downloadFile:file
isExportingToPDF:isExportingToPDF
toDestinationURL:savePanel.URL];
}
}];
}
- (void)downloadFile:(GTLRDrive_File *)file
isExportingToPDF:(BOOL)isExporting
toDestinationURL:(NSURL *)destinationURL {
GTLRDriveService *service = self.driveService;
GTLRQuery *query;
if (isExporting) {
// Note: this will fail if the file type cannot be converted to PDF.
query = [GTLRDriveQuery_FilesExport queryForMediaWithFileId:file.identifier
mimeType:@"application/pdf"];
} else {
// Download original file. This will fail if the file type
// cannot be downloaded in its native server format.
query = [GTLRDriveQuery_FilesGet queryForMediaWithFileId:file.identifier];
}
// GTLR queries are suitable for downloading and exporting small files.
//
// For large files, apps typically will want to monitor the progress of a download
// or to download with a Range request header to specify a subset of bytes.
//
// To download large files, get the full NSURLRequest from the GTLR query instead of
// executing the query.
//
// Here's how to download with a GTMSessionFetcher. The fetcher will use the authorizer that's
// attached to the GTLR service's fetcherService.
//
// NSURLRequest *downloadRequest = [service requestForQuery:query];
// GTMSessionFetcher *fetcher = [service.fetcherService fetcherWithRequest:downloadRequest];
//
// [fetcher setCommentWithFormat:@"Downloading %@", file.name];
// fetcher.destinationFileURL = destinationURL;
//
// [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
// if (error == nil) {
// NSLog(@"Download succeeded.");
//
// // With a destinationFileURL property set, the fetcher's callback
// // data parameter here will be nil.
// }
// }];
[service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDataObject *object,
NSError *callbackError) {
NSError *errorToReport = callbackError;
NSError *writeError;
if (callbackError == nil) {
BOOL didSave = [object.data writeToURL:destinationURL
options:NSDataWritingAtomic
error:&writeError];
if (!didSave) {
errorToReport = writeError;
}
}
if (errorToReport == nil) {
// Successfully saved the file.
//
// Since a downloadPath property was specified, the data argument is
// nil, and the file data has been written to disk.
[self displayAlert:@"Downloaded"
format:@"%@", destinationURL.path];
} else {
[self displayAlert:@"Error Downloading File"
format:@"%@", errorToReport];
}
}];
}
- (NSString *)extensionForMIMEType:(NSString *)mimeType {
// Try to convert a MIME type to an extension using the Mac's type identifiers.
NSString *result = nil;
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,
(__bridge CFStringRef)mimeType, NULL);
if (uti) {
CFStringRef cfExtn = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
if (cfExtn) {
result = CFBridgingRelease(cfExtn);
}
CFRelease(uti);
}
return result;
}
- (IBAction)uploadFileClicked:(id)sender {
// Ask the user to choose a file.
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
openPanel.prompt = @"Upload";
openPanel.canChooseDirectories = NO;
[openPanel beginSheetModalForWindow:[self window]
completionHandler:^(NSInteger result) {
// Callback.
if (result == NSFileHandlingPanelOKButton) {
// The user chose a file and clicked OK.
//
// Start uploading (deferred briefly since
// we currently have a sheet displayed).
NSString *path = [[openPanel URL] path];
[self performSelector:@selector(uploadFileAtPath:)
withObject:path
afterDelay:0.1];
}
}];
}
- (IBAction)pauseUploadClicked:(id)sender {
if ([_uploadFileTicket isUploadPaused]) {
[_uploadFileTicket resumeUpload];
} else {
[_uploadFileTicket pauseUpload];
}
[self updateUI];
}
- (IBAction)stopUploadClicked:(id)sender {
[_uploadFileTicket cancelTicket];
_uploadFileTicket = nil;
[self updateUI];
}
- (IBAction)createFolderClicked:(id)sender {
[self createAFolder];
}
- (IBAction)APIConsoleClicked:(id)sender {
NSURL *url = [NSURL URLWithString:@"https://console.developers.google.com/"];
[[NSWorkspace sharedWorkspace] openURL:url];
}
- (IBAction)loggingCheckboxClicked:(NSButton *)sender {
[GTMSessionFetcher setLoggingEnabled:[sender state]];
}
#pragma mark -
// Get a service object with the current username/password
//
// A "service" object handles networking tasks. Service objects
// contain user authentication information as well as networking
// state information (such as cookies and the "last modified" date for
// fetched data.)
- (GTLRDriveService *)driveService {
static GTLRDriveService *service;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
service = [[GTLRDriveService alloc] init];
// Turn on the library's shouldFetchNextPages feature to ensure that all items
// are fetched. This applies to queries which return an object derived from
// GTLRCollectionObject.
service.shouldFetchNextPages = YES;
// Have the service object set tickets to retry temporary error conditions
// automatically
service.retryEnabled = YES;
});
return service;
}
- (GTLRDrive_File *)selectedFileListEntry {
NSInteger rowIndex = [_fileListTable selectedRow];
if (rowIndex > -1) {
GTLRDrive_File *item = _fileList.files[rowIndex];
return item;
}
return nil;
}
- (id)detailCollectionArray {
NSInteger segment = [_segmentedControl selectedSegment];
switch (segment) {
case kRevisionsSegment:
return _revisionList.revisions;
case kPermissionsSegment:
return _permissionList.permissions;
case kChildrenSegment:
return _childList.files;
case kParentsSegment:
return _parentsList;
default:
return nil;
}
}
- (id)selectedDetailItem {
NSInteger rowIndex = [_detailTable selectedRow];
if (rowIndex > -1) {
NSArray *array = [self detailCollectionArray];
GTLRObject *item = array[rowIndex];
return item;
}
return nil;
}
- (NSError *)detailsError {
// First, check if the query execution succeeded.
NSError *error = _detailsFetchError;
if (error == nil) {
// Next, check if there was an error for the selected detail.
NSInteger segment = [_segmentedControl selectedSegment];
switch (segment) {
case kRevisionsSegment:
return _revisionListFetchError;
case kPermissionsSegment:
return _permissionListFetchError;
case kChildrenSegment:
return _childListFetchError;
case kParentsSegment:
return _parentsListFetchError;
default:
return nil;
}
}
return nil;
}
- (NSString *)descriptionForFileID:(NSString *)fileID {
NSArray *files = _fileList.files;
GTLRDrive_File *file = [GTLRUtilities firstObjectFromArray:files
withValue:fileID
forKeyPath:@"identifier"];
if (file) {
return file.name;
} else {
// Can't find the file by its identifier.
return [NSString stringWithFormat:@"<%@>", fileID];
}
}
- (NSString *)descriptionForDetailItem:(id)item {
if ([item isKindOfClass:[GTLRDrive_Revision class]]) {
return ((GTLRDrive_Revision *)item).modifiedTime.stringValue;
} else if ([item isKindOfClass:[GTLRDrive_Permission class]]) {
return ((GTLRDrive_Permission *)item).displayName;
} else if ([item isKindOfClass:[GTLRDrive_File class]]) {
NSString *fileID = ((GTLRDrive_File *)item).identifier;
return [self descriptionForFileID:fileID];
} else if ([item isKindOfClass:[NSString class]]) {
// item is probably a file ID
return [self descriptionForFileID:item];
}
return nil;
}
#pragma mark -
#pragma mark Fetch File List
- (void)fetchFileList {
_fileList = nil;
_fileListFetchError = nil;
GTLRDriveService *service = self.driveService;
GTLRDriveQuery_FilesList *query = [GTLRDriveQuery_FilesList query];
// Because GTLRDrive_FileList is derived from GTLCollectionObject and the service
// property shouldFetchNextPages is enabled, this may do multiple fetches to
// retrieve all items in the file list.
// Google APIs typically allow the fields returned to be limited by the "fields" property.
// The Drive API uses the "fields" property differently by not sending most of the requested
// resource's fields unless they are explicitly specified.
query.fields = @"kind,nextPageToken,files(mimeType,id,kind,name,webViewLink,thumbnailLink,trashed)";
_fileListTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDrive_FileList *fileList,
NSError *callbackError) {
// Callback
self->_fileList = fileList;
self->_fileListFetchError = callbackError;
self->_fileListTicket = nil;
[self updateUI];
}];
[self updateUI];
}
#pragma mark Fetch File Details
- (void)fetchSelectedFileDetails {
_revisionList = nil;
_revisionListFetchError = nil;
_permissionList = nil;
_permissionListFetchError = nil;
_childList = nil;
_childListFetchError = nil;
_parentsList = nil;
_parentsListFetchError = nil;
_detailsFetchError = nil;
GTLRDriveService *service = self.driveService;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *fileID = selectedFile.identifier;
if (fileID) {
// Rather than make separate fetches for each kind of detail for the
// selected file, we'll make a single batch query to etch the various
// details. Each query in the batch will have its own result or error,
// and the batch query execution itself may fail with an error.
GTLRDriveQuery_RevisionsList *revisionQuery = [GTLRDriveQuery_RevisionsList queryWithFileId:fileID];
revisionQuery.completionBlock = ^(GTLRServiceTicket *callbackTicket,
GTLRDrive_RevisionList *obj,
NSError *callbackError) {
self->_revisionList = obj;
self->_revisionListFetchError = callbackError;
};
GTLRDriveQuery_PermissionsList *permissionQuery =
[GTLRDriveQuery_PermissionsList queryWithFileId:fileID];
permissionQuery.completionBlock = ^(GTLRServiceTicket *callbackTicket,
GTLRDrive_PermissionList *obj,
NSError *callbackError) {
self->_permissionList = obj;
self->_permissionListFetchError = callbackError;
};
GTLRDriveQuery_FilesList *childQuery = [GTLRDriveQuery_FilesList query];
childQuery.q = [NSString stringWithFormat:@"'%@' in parents", fileID];
// Accumulate additional pages of results for this query, if necessary.
childQuery.completionBlock = ^(GTLRServiceTicket *callbackTicket, GTLRDrive_FileList *obj,
NSError *callbackError) {
self->_childList = obj;
self->_childListFetchError = callbackError;
};
// Note: The fields property in Google APIs is supposed to restrict
// the fields returned for a partial query, though in the v3 Drive API
// it is required here to return the parents field.
GTLRDriveQuery_FilesGet *parentsQuery = [GTLRDriveQuery_FilesGet queryWithFileId:fileID];
parentsQuery.fields = @"parents";
parentsQuery.completionBlock = ^(GTLRServiceTicket *callbackTicket, GTLRDrive_File *obj,
NSError *callbackError) {
self->_parentsList = obj.parents;
self->_parentsListFetchError = callbackError;
};
// Combine the separate queries into one batch.
GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery];
[batchQuery addQuery:revisionQuery];
[batchQuery addQuery:permissionQuery];
[batchQuery addQuery:childQuery];
[batchQuery addQuery:parentsQuery];
_detailsTicket = [service executeQuery:batchQuery
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRBatchResult *batchResult,
NSError *callbackError) {
// Callback
//
// The batch query execution completionHandler runs after the individual
// query completion handlers have been called.
self->_detailsTicket = nil;
self->_detailsFetchError = callbackError;
[self updateUI];
}];
[self updateUI];
}
}
#pragma mark Delete a File
- (void)deleteSelectedFile {
GTLRDriveService *service = self.driveService;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *fileID = selectedFile.identifier;
if (fileID) {
GTLRDriveQuery_FilesDelete *query = [GTLRDriveQuery_FilesDelete queryWithFileId:fileID];
_editFileListTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
id nilObject,
NSError *callbackError) {
// Callback
self->_editFileListTicket = nil;
if (callbackError == nil) {
[self displayAlert:@"Deleted"
format:@"Deleted \"%@\"",
selectedFile.name];
[self updateUI];
[self fetchFileList];
} else {
[self displayAlert:@"Delete Failed"
format:@"%@", callbackError];
}
}];
}
}
#pragma mark Toggle Trash State
- (void)changeTrashStateForSelectedFile {
GTLRDriveService *service = self.driveService;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *fileID = selectedFile.identifier;
if (fileID) {
GTLRDriveQuery *query;
BOOL isInTrash = selectedFile.trashed.boolValue;
GTLRDrive_File *updateFile = [GTLRDrive_File object];
updateFile.trashed = isInTrash ? @NO : @YES;
query = [GTLRDriveQuery_FilesUpdate queryWithObject:updateFile
fileId:fileID
uploadParameters:nil];
_editFileListTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDrive_File *updatedObject,
NSError *callbackError) {
// Callback
self->_editFileListTicket = nil;
if (callbackError == nil) {
NSString *fmt = (isInTrash ? @"Moved \"%@\" out of trash" : @"Moved \"%@\" to trash");
[self displayAlert:@"Updated"
format:fmt, selectedFile.name];
[self updateUI];
[self fetchFileList];
} else {
[self displayAlert:@"Trash Change Failed"
format:@"%@", callbackError];
}
}];
}
}
#pragma mark Duplicate a File
- (void)duplicateSelectedFile {
GTLRDriveService *service = self.driveService;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *fileID = selectedFile.identifier;
if (fileID) {
// Make a file object with the title to use for the duplicate.
GTLRDrive_File *fileObj = [GTLRDrive_File object];
fileObj.name = [NSString stringWithFormat:@"%@ copy", selectedFile.name];
GTLRDriveQuery_FilesCopy *query = [GTLRDriveQuery_FilesCopy queryWithObject:fileObj
fileId:fileID];
_editFileListTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDrive_File *copiedFile,
NSError *callbackError) {
// Callback
self->_editFileListTicket = nil;
if (callbackError == nil) {
[self displayAlert:@"Copied"
format:@"Created copy \"%@\"",
copiedFile.name];
[self updateUI];
[self fetchFileList];
} else {
[self displayAlert:@"Copy failed"
format:@"%@", callbackError];
}
}];
}
}
#pragma mark New Folder
- (void)createAFolder {
GTLRDriveService *service = self.driveService;
GTLRDrive_File *folderObj = [GTLRDrive_File object];
folderObj.name = [NSString stringWithFormat:@"New Folder %@", [NSDate date]];
folderObj.mimeType = @"application/vnd.google-apps.folder";
// To create a folder in a specific parent folder, specify the addParents property
// for the query.
GTLRDriveQuery_FilesCreate *query = [GTLRDriveQuery_FilesCreate queryWithObject:folderObj
uploadParameters:nil];
_editFileListTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDrive_File *folderItem,
NSError *callbackError) {
// Callback
self->_editFileListTicket = nil;
if (callbackError == nil) {
[self displayAlert:@"Created"
format:@"Created folder \"%@\"",
folderItem.name];
[self updateUI];
[self fetchFileList];
} else {
[self displayAlert:@"Create Folder Failed"
format:@"%@", callbackError];
}
}];
}
#pragma mark Uploading
- (void)uploadFileAtPath:(NSString *)path {
NSURL *fileToUploadURL = [NSURL fileURLWithPath:path];
NSError *fileError;
if (![fileToUploadURL checkPromisedItemIsReachableAndReturnError:&fileError]) {
// Could not read file data.
[self displayAlert:@"No Upload File Found"
format:@"Path: %@", path];
return;
}
// Queries that support file uploads take an uploadParameters object.
// The uploadParameters include the MIME type of the file being uploaded,
// and either an NSData with the file contents, or a URL for
// the file path.
GTLRDriveService *service = self.driveService;
NSString *filename = [path lastPathComponent];
NSString *mimeType = [self MIMETypeFileName:filename
defaultMIMEType:@"binary/octet-stream"];
GTLRUploadParameters *uploadParameters =
[GTLRUploadParameters uploadParametersWithFileURL:fileToUploadURL
MIMEType:mimeType];
GTLRDrive_File *newFile = [GTLRDrive_File object];
newFile.name = filename;
GTLRDriveQuery_FilesCreate *query = [GTLRDriveQuery_FilesCreate queryWithObject:newFile
uploadParameters:uploadParameters];
NSProgressIndicator *uploadProgressIndicator = _uploadProgressIndicator;
query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *callbackTicket,
unsigned long long numberOfBytesRead,
unsigned long long dataLength) {
uploadProgressIndicator.maxValue = (double)dataLength;
uploadProgressIndicator.doubleValue = (double)numberOfBytesRead;
};
_uploadFileTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDrive_File *uploadedFile,
NSError *callbackError) {
// Callback
self->_uploadFileTicket = nil;
if (callbackError == nil) {
[self displayAlert:@"Created"
format:@"Uploaded file \"%@\"",
uploadedFile.name];
[self fetchFileList];
} else {
[self displayAlert:@"Upload Failed"
format:@"%@", callbackError];
}
self->_uploadProgressIndicator.doubleValue = 0.0;
[self updateUI];
}];
[self updateUI];
}
- (NSString *)MIMETypeFileName:(NSString *)path
defaultMIMEType:(NSString *)defaultType {
NSString *result = defaultType;
NSString *extension = [path pathExtension];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
(__bridge CFStringRef)extension, NULL);
if (uti) {
CFStringRef cfMIMEType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
if (cfMIMEType) {
result = CFBridgingRelease(cfMIMEType);
}
CFRelease(uti);
}
return result;
}
#pragma mark -
#pragma mark Sign In
- (void)runSigninThenHandler:(void (^)(void))handler {
// Applications should have client ID hardcoded into the source
// but the sample application asks the developer for the strings.
// Client secret is now left blank.
NSString *clientID = [_clientIDField stringValue];
NSString *clientSecret = [_clientSecretField stringValue];
if (clientID.length == 0) {
// Remind the developer that client ID is needed. Client secret is now left blank
[_clientIDButton performSelector:@selector(performClick:)
withObject:self
afterDelay:0.5];
return;
}
NSURL *successURL = [NSURL URLWithString:kSuccessURLString];
// Starts a loopback HTTP listener to receive the code, gets the redirect URI to be used.
_redirectHTTPHandler = [[OIDRedirectHTTPHandler alloc] initWithSuccessURL:successURL];
NSError *error;
NSURL *localRedirectURI = [_redirectHTTPHandler startHTTPListener:&error];
if (!localRedirectURI) {
NSLog(@"Unexpected error starting redirect handler %@", error);
return;
}
// Builds authentication request.
OIDServiceConfiguration *configuration =
[GTMAppAuthFetcherAuthorization configurationForGoogle];
// Applications that only need to access files created by this app should
// use the kGTLRAuthScopeDriveFile scope.
NSArray<NSString *> *scopes = @[ kGTLRAuthScopeDrive, OIDScopeEmail ];
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:clientID
clientSecret:clientSecret
scopes:scopes
redirectURL:localRedirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
__weak __typeof(self) weakSelf = self;
_redirectHTTPHandler.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
// Using weakSelf/strongSelf pattern to avoid retaining self as block execution is indeterminate
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Brings this app to the foreground.
[[NSRunningApplication currentApplication]
activateWithOptions:(NSApplicationActivateAllWindows |
NSApplicationActivateIgnoringOtherApps)];
if (authState) {
// Creates a GTMAppAuthFetcherAuthorization object for authorizing requests.
GTMAppAuthFetcherAuthorization *gtmAuthorization =
[[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
// Sets the authorizer on the GTLRYouTubeService object so API calls will be authenticated.
strongSelf.driveService.authorizer = gtmAuthorization;
// Serializes authorization to keychain in GTMAppAuth format.
[GTMAppAuthFetcherAuthorization saveAuthorization:gtmAuthorization
toKeychainForName:kGTMAppAuthKeychainItemName];
// Executes post sign-in handler.
if (handler) handler();
} else {
strongSelf->_fileListFetchError = error;
[strongSelf updateUI];
}
}];
}
#pragma mark -
#pragma mark UI
- (void)updateUI {
BOOL isSignedIn = [self isSignedIn];
NSString *username = [self signedInUsername];
_signedInButton.title = (isSignedIn ? @"Sign Out" : @"Sign In");
_signedInField.stringValue = (isSignedIn ? username : @"No");
//
// File list table
//
[_fileListTable reloadData];
if (_fileListTicket != nil || _editFileListTicket != nil) {
[_fileListProgressIndicator startAnimation:self];
} else {
[_fileListProgressIndicator stopAnimation:self];
}
// Get the description of the selected item, or the feed fetch error
NSString *resultStr = @"";
if (_fileListFetchError) {
// Display the error
resultStr = [_fileListFetchError description];
// Also display any server data present
NSDictionary *errorInfo = [_fileListFetchError userInfo];
NSData *errData = errorInfo[kGTMSessionFetcherStatusDataKey];
if (errData) {
NSString *dataStr = [[NSString alloc] initWithData:errData
encoding:NSUTF8StringEncoding];
resultStr = [resultStr stringByAppendingFormat:@"\n%@", dataStr];
}
} else {
// Display the selected item
GTLRDrive_File *item = [self selectedFileListEntry];
if (item) {
resultStr = [item description];
}
}
_fileListResultTextField.string = resultStr;
[self updateThumbnailImage];
//
// Details table
//
[_detailTable reloadData];
if (_detailsTicket != nil) {
[_detailProgressIndicator startAnimation:self];
} else {
[_detailProgressIndicator stopAnimation:self];
}
// Get the description of the selected item, or the feed fetch error
resultStr = @"";
NSError *error = [self detailsError];
if (error) {
resultStr = [error description];
} else {
id item = [self selectedDetailItem];
if (item) {
resultStr = [item description];
}
}
_detailResultTextField.string = resultStr;
// Update the counts in the segmented control
NSUInteger numberOfRevisions = _revisionList.revisions.count;
NSUInteger numberOfPermissions = _permissionList.permissions.count;
NSUInteger numberOfChildren = _childList.files.count;
NSUInteger numberOfParents = _parentsList.count;
NSString *revisionsStr = [NSString stringWithFormat:@"Revisions %tu", numberOfRevisions];
NSString *permissionsStr = [NSString stringWithFormat:@"Permissions %tu", numberOfPermissions];
NSString *childrenStr = [NSString stringWithFormat:@"Children %tu", numberOfChildren];
NSString *parentsStr = [NSString stringWithFormat:@"Parents %tu", numberOfParents];
[_segmentedControl setLabel:revisionsStr forSegment:kRevisionsSegment];
[_segmentedControl setLabel:permissionsStr forSegment:kPermissionsSegment];
[_segmentedControl setLabel:childrenStr forSegment:kChildrenSegment];
[_segmentedControl setLabel:parentsStr forSegment:kParentsSegment];
// Enable buttons
BOOL isFetchingFileList = (_fileListTicket != nil);
BOOL isEditingFileList = (_editFileListTicket != nil);
_fileListCancelButton.enabled = (isFetchingFileList || isEditingFileList);
BOOL isFetchingDetails = (_detailsTicket != nil);
_detailCancelButton.enabled = isFetchingDetails;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *webViewLink = selectedFile.webViewLink;
BOOL isFileViewable = (webViewLink != nil);
_viewButton.enabled = isFileViewable;
BOOL isFileSelected = (selectedFile != nil);
_deleteButton.enabled = isFileSelected;
_downloadButton.enabled = isFileSelected;
_exportAsPDFButton.enabled = isFileSelected;
_trashButton.enabled = isFileSelected;
BOOL isInTrash = selectedFile.trashed.boolValue;
NSString *trashTitle = (isInTrash ? @"Untrash" : @"Trash");
_trashButton.title = trashTitle;
_duplicateButton.enabled = isFileSelected;
BOOL hasFileList = (_fileList != nil);
_newFolderButton.enabled = hasFileList;
BOOL isUploading = (_uploadFileTicket != nil);
_uploadButton.enabled = (hasFileList && !isUploading);
_pauseUploadButton.enabled = isUploading;
_stopUploadButton.enabled = isUploading;
BOOL isUploadPaused = [_uploadFileTicket isUploadPaused];
NSString *pauseTitle = (isUploadPaused ? @"Resume" : @"Pause");
_pauseUploadButton.title = pauseTitle;
// Show or hide the text indicating that the client ID or client secret are
// needed
BOOL hasClientIDStrings = _clientIDField.stringValue.length > 0
&& _clientSecretField.stringValue.length > 0;
_clientIDRequiredTextField.hidden = hasClientIDStrings;
}
- (void)updateThumbnailImage {
// We will fetch the thumbnail image if its URL is different from the one
// currently displayed.
static NSString *gDisplayedURLStr = nil;
GTLRDrive_File *selectedFile = [self selectedFileListEntry];
NSString *thumbnailURLStr = selectedFile.thumbnailLink;
if (!GTLR_AreEqualOrBothNil(gDisplayedURLStr, thumbnailURLStr)) {
_thumbnailView.image = nil;
gDisplayedURLStr = [thumbnailURLStr copy];
if (thumbnailURLStr) {
GTMSessionFetcher *fetcher =
[self.driveService.fetcherService fetcherWithURLString:thumbnailURLStr];
fetcher.authorizer = self.driveService.authorizer;
[fetcher setCommentWithFormat:@"Thumbnail for \"%@\"", selectedFile.name];
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *fetchError) {
if (data) {
NSImage *image = [[NSImage alloc] initWithData:data];
if (image) {
self->_thumbnailView.image = image;
} else {
NSLog(@"Failed to make image from %tu bytes for \"%@\"",
data.length, selectedFile.name);
}
} else {
NSLog(@"Failed to fetch thumbnail for \"%@\", %@",
selectedFile.name, fetchError);
}
}];
}
}
}
- (void)displayAlert:(NSString *)title format:(NSString *)format, ... {
NSString *result = @"";
if (format) {
va_list argList;
va_start(argList, format);
result = [[NSString alloc] initWithFormat:format
arguments:argList];
va_end(argList);
}
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = title;
alert.informativeText = result;
[alert beginSheetModalForWindow:[self window]
completionHandler:nil];
}
#pragma mark Client ID Sheet
// Client ID and Client Secret Sheet
//
// Sample apps need this sheet to ask for the client ID and client secret
// strings
//
// Your application will just hardcode the client ID and client secret strings
// into the source rather than ask the user for them.
//
// The string values are obtained from the API Console,
// https://console.developers.google.com/
- (IBAction)clientIDClicked:(NSButton *)sender {
// Show the sheet for developers to enter their client ID and client secret
[self.window beginSheet:_clientIDSheet completionHandler:nil];
}
- (IBAction)clientIDDoneClicked:(NSButton *)sender {
[self.window endSheet:sender.window];
}
#pragma mark Text field delegate methods
- (void)controlTextDidChange:(NSNotification *)note {
[self updateUI]; // enable and disable buttons
}
#pragma mark TableView delegate and data source methods
- (void)tableViewSelectionDidChange:(NSNotification *)notification {
[self updateUI];
if ([notification object] == _fileListTable) {
[self fetchSelectedFileDetails];
}
}
// Table view data source methods
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
NSArray *array;
if (tableView == _fileListTable) {
array = _fileList.files;
} else {
array = [self detailCollectionArray];
}
return array.count;
}
- (id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
if (tableView == _fileListTable) {
GTLRDrive_File *file = _fileList.files[row];
return [self fileTitleWithLabelsForFile:file];
} else {
NSArray *array = [self detailCollectionArray];
id item = array[row];
return [self descriptionForDetailItem:item];
}
}
- (NSString *)fileTitleWithLabelsForFile:(GTLRDrive_File *)file {
NSMutableString *title = [NSMutableString stringWithString:file.name];
if (file.starred.boolValue) {
[title appendString:@" \u2605"]; // star character
}
if (file.trashed.boolValue) {
[title insertString:@"\u2717 " atIndex:0]; // X character
}
if (file.viewersCanCopyContent.boolValue) {
[title appendString:@" \u21DF"]; // crossed down arrow character
}
return title;
}
@end