2

Has anyone successfully implemented the "open" action for a file provider app extension? I've gotten as far as being able to read a file when the user initially selects the file in the document picker extension (essentially, this is the "import" action). But anything beyond that fails. Here are the issues I've run into:

  • The app deadlocks if I use an NSFileCoordinator.
  • If I save the URL and try to either read or write to it later, the call to startAccessingSecurityScopedResource returns NO. This works if I use bookmarks.
  • If I try bookmarkDataWithOptions:, I get back Error Domain=NSCocoaErrorDomain Code=260 "The operation couldn’t be completed. (Cocoa error 260.)". This works if I create the bookmarks inside the security scope.

Here is the template that is created for startProvidingItemAtURL: when a file provider extension is created:

- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler {
    // Should ensure that the actual file is in the position returned by URLForItemWithIdentifier:, then call the completion handler
    NSError* error = nil;
    __block NSError* fileError = nil;

    NSData * fileData = [NSData data];
    // TODO: get the contents of file at <url> from model

    [self.fileCoordinator coordinateWritingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
        [fileData writeToURL:newURL options:0 error:&fileError];
    }];
    if (error!=nil) {
        completionHandler(error);
    } else {
        completionHandler(fileError);
    }
}

But the extension deadlocks when I use the file coordinator. Also, the documentation for startProvidingItemAtURL: says "Note Do not use file coordination inside this method." so I've taken it out.

In the other app, this is what I am doing to read that file for the first time and then create a bookmark for it:

// Start accessing the security scoped resource.
[url startAccessingSecurityScopedResource];

void (^accessor)(NSURL *) = ^void(NSURL *url) {
  // If the file is missing, create a default here. This really should be done inside
  // the FileProvider method startProvidingItemAtURL:. Unfortunately, that method does
  // not get called unless we use use the file coordinator, which can deadlock the app.
  if (![url checkResourceIsReachableAndReturnError:nil]) {
    // TODO: Create a real default file here.
    [[NSFileManager defaultManager] createFileAtPath:url.path
                                            contents:nil
                                          attributes:nil];
  }

  // TODO: Do something with this file.
};

#ifdef USE_FILE_COORDINATOR
NSFileCoordinator *fileCoordinator = [NSFileCoordinator new];
[fileCoordinator coordinateReadingItemAtURL:url
                                    options:NSFileCoordinatorReadingWithoutChanges
                                      error:NULL
                                 byAccessor:accessor];
#else
accessor(url);
#endif

// Store a bookmark for the url in the defaults so we can use it later.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSError *error = nil;
NSURLBookmarkCreationOptions options = 0;
#ifdef NSURLBookmarkCreationWithSecurityScope
options |= NSURLBookmarkCreationWithSecurityScope;
#endif
NSData *bookmarkData = [url bookmarkDataWithOptions:options
                     includingResourceValuesForKeys:nil
                                      relativeToURL:nil
                                                error:&error];
if (error) {
  NSLog(@"ERROR: %@", error);
}
[defaults setObject:bookmarkData forKey:@"BookmarkDataKey"];

// Stop accessing the security scoped resource.
[url stopAccessingSecurityScopedResource];

And finally, to use the bookmark later, I am doing the following:

// Get the bookmark from the defaults file.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *bookmarkData = [defaults objectForKey:@"BookmarkDataKey"];
if (bookmarkData) {
  // Convert the bookmark into a URL.
  NSError *error;
  BOOL bookmarkIsStale;
  NSURLBookmarkResolutionOptions options = NSURLBookmarkResolutionWithoutUI;
#ifdef NSURLBookmarkResolutionWithSecurityScope
  options |= NSURLBookmarkResolutionWithSecurityScope;
#endif

  NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
                                         options:options
                                   relativeToURL:nil
                             bookmarkDataIsStale:&bookmarkIsStale
                                           error:&error];

  // Get the data from the URL.
  BOOL securitySucceeded = [url startAccessingSecurityScopedResource];
  if (securitySucceeded) {
    NSString *message = [NSString stringWithFormat:@"Random number: #%d", arc4random() % 10000];
    NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
    NSError *fileError = nil;
    [fileData writeToURL:url options:0 error:&fileError];

    [url stopAccessingSecurityScopedResource];
  }
}

The second app also sometimes deadlocks if I use file coordination. So should I just not use file coordination in the second app as well? The problem is that if I don't use file coordination, then startProvidingItemAtURL: in the File Provider extension never seems to get called.

Also, the documentation says to use NSURLBookmarkCreationWithSecurityScope but this is undefined for iOS. The same goes for NSURLBookmarkResolutionWithSecurityScope. Should I just use the OS X values or just not use them?

  • Please add the code so that I can help you. File provider is bit complicated. Even we tracked it by trail and error methods – Pavan Apr 02 '15 at 23:26
  • Thank you for your response. In the end, I think I got it working by removing the file coordinator and ignoring the security scope bookmark constants. – Mach Kobayashi Apr 05 '15 at 02:50
  • Hi, Can you please share a simple demo on github. Thanks, – Durgaprasad Jun 10 '15 at 12:20

2 Answers2

2

In the end, I think I've got it working by removing file coordination everywhere and ignoring the security scope bookmark constants. Here is what I used for startProvidingItemAtURL: in the file provider extension:

- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler {
  // If the file doesn't exist then create one.
  if (![url checkResourceIsReachableAndReturnError:nil]) {
    __block NSError *fileError = nil;
    NSString *message = @"This is a test message";
    NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
    [fileData writeToURL:url options:0 error:&fileError];
    completionHandler(fileError);
  }
}

In the other app, this is what I am doing to read that file for the first time and then create a bookmark for it:

// Start accessing the security scoped resource.
[url startAccessingSecurityScopedResource];

// If the file is missing, create a default here. This really should be done inside
// the FileProvider method startProvidingItemAtURL:. Unfortunately, that method does
// not get called unless we use use the file coordinator, which can deadlock the app.
if (![url checkResourceIsReachableAndReturnError:nil]) {
  // TODO: Create a real default file here.
  [[NSFileManager defaultManager] createFileAtPath:url.path
                                          contents:nil
                                        attributes:nil];
// TODO: Do something with this file.

// Store a bookmark for the url in the defaults so we can use it later.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSError *error = nil;
NSURLBookmarkCreationOptions options = 0;
#ifdef NSURLBookmarkCreationWithSecurityScope
options |= NSURLBookmarkCreationWithSecurityScope;
#endif
NSData *bookmarkData = [url bookmarkDataWithOptions:options
                     includingResourceValuesForKeys:nil
                                      relativeToURL:nil
                                                error:&error];
if (error) {
  NSLog(@"ERROR: %@", error);
}
[defaults setObject:bookmarkData forKey:@"BookmarkDataKey"];

// Stop accessing the security scoped resource.
[url stopAccessingSecurityScopedResource];

And finally, to use the bookmark later, I am doing the following:

// Get the bookmark from the defaults file.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *bookmarkData = [defaults objectForKey:@"BookmarkDataKey];
if (bookmarkData) {
  // Convert the bookmark into a URL.
  NSError *error;
  BOOL bookmarkIsStale;
  NSURLBookmarkResolutionOptions options = NSURLBookmarkResolutionWithoutUI;
#ifdef NSURLBookmarkResolutionWithSecurityScope
  options |= NSURLBookmarkResolutionWithSecurityScope;
#endif

  NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
                                         options:options
                                   relativeToURL:nil
                             bookmarkDataIsStale:&bookmarkIsStale
                                           error:&error];

  // Get the data from the URL.
  BOOL securitySucceeded = [url startAccessingSecurityScopedResource];
  if (securitySucceeded) {
    NSString *message = [NSString stringWithFormat:@"Random number: #%d", arc4random() % 10000];
    NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
    NSError *fileError = nil;
    [fileData writeToURL:url options:0 error:&fileError];

    [url stopAccessingSecurityScopedResource];
  }
}
0

Your are not suppose to call file coordinator calls in: staringProvidingItemsAtUrl
Check the apple comments for the method and it says:
- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *error))completionHandler
Note
Do not use file coordination inside this method. The system already guarantees that no other process can access the file while this method is executing.

Once you remove:
[self.fileCoordinator coordinateWritingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL)
The deadlock should disappear.
Also: NSURLBookmarkCreationWithSecurityScope
does not apply to IOS and IOS does not have this option. You don't need this option for IOS. The apple document is VERY confusion on this.

us_david
  • 4,431
  • 35
  • 29