From 193615a1584aafc997a56108023519080c19b828 Mon Sep 17 00:00:00 2001 From: Diego Sanchez Date: Sat, 22 Dec 2018 00:16:33 -0800 Subject: [PATCH] Revert D13513777: Replace ALAssets* with PHPhoto* in RCTCameraRoll Differential Revision: D13513777 Original commit changeset: 3f0c4ae25982 fbshipit-source-id: a3a868db96eb65415a5512974da1a2f695773065 --- .../RCTAssetsLibraryRequestHandler.h | 11 +- .../RCTAssetsLibraryRequestHandler.m | 151 ++++---- Libraries/CameraRoll/RCTCameraRollManager.h | 8 +- Libraries/CameraRoll/RCTCameraRollManager.m | 362 ++++++++---------- .../CameraRoll/RCTPhotoLibraryImageLoader.m | 2 +- Libraries/Image/RCTImageLoader.m | 2 +- 6 files changed, 265 insertions(+), 271 deletions(-) diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h index 7a0f0635ee7b75..7ccba500f59f0b 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h @@ -8,8 +8,17 @@ #import #import -@class PHPhotoLibrary; +@class ALAssetsLibrary; @interface RCTAssetsLibraryRequestHandler : NSObject @end + +@interface RCTBridge (RCTAssetsLibraryImageLoader) + +/** + * The shared asset library instance. + */ +@property (nonatomic, readonly) ALAssetsLibrary *assetsLibrary; + +@end diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m index cdc6cb9f203ebc..663598ceaea0b7 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m @@ -11,26 +11,41 @@ #import #import -#import +#import #import #import #import @implementation RCTAssetsLibraryRequestHandler +{ + ALAssetsLibrary *_assetsLibrary; +} RCT_EXPORT_MODULE() +@synthesize bridge = _bridge; +static Class _ALAssetsLibrary = nil; +static void ensureAssetsLibLoaded(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); +#pragma unused(handle) + _ALAssetsLibrary = objc_getClass("ALAssetsLibrary"); + }); +} +- (ALAssetsLibrary *)assetsLibrary +{ + ensureAssetsLibLoaded(); + return _assetsLibrary ?: (_assetsLibrary = [_ALAssetsLibrary new]); +} + #pragma mark - RCTURLRequestHandler - (BOOL)canHandleRequest:(NSURLRequest *)request { - if (![PHAsset class]) { - return NO; - } - - return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame - || [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame; + return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; } - (id)sendRequest:(NSURLRequest *)request @@ -40,75 +55,58 @@ - (id)sendRequest:(NSURLRequest *)request void (^cancellationBlock)(void) = ^{ atomic_store(&cancelled, YES); }; - - if (!request.URL) { - NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"]; - [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; - return cancellationBlock; - } - - PHFetchResult *fetchResult; - - if ([request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) { - // Fetch assets using PHAsset localIdentifier (recommended) - NSString *const localIdentifier = [request.URL.absoluteString substringFromIndex:@"ph://".length]; - fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil]; - } else if ([request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) { - // This is the older, deprecated way of fetching assets from assets-library - // using the "assets-library://" protocol - fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[request.URL] options:nil]; - } else { - NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", request.URL]; - [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; - return cancellationBlock; - } - - if (![fetchResult firstObject]) { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" - " at URL %@ with no error message.", request.URL]; - NSError *error = RCTErrorWithMessage(errorMessage); - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - return cancellationBlock; - } - - if (atomic_load(&cancelled)) { - return cancellationBlock; - } - - PHAsset *const _Nonnull asset = [fetchResult firstObject]; - - // By default, allow downloading images from iCloud - PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new]; - requestOptions.networkAccessAllowed = YES; - - [[PHImageManager defaultManager] requestImageDataForAsset:asset - options:requestOptions - resultHandler:^(NSData * _Nullable imageData, - NSString * _Nullable dataUTI, - UIImageOrientation orientation, - NSDictionary * _Nullable info) { - NSError *const error = [info objectForKey:PHImageErrorKey]; - if (error) { - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + + [[self assetsLibrary] assetForURL:request.URL resultBlock:^(ALAsset *asset) { + if (atomic_load(&cancelled)) { return; } - NSInteger const length = [imageData length]; - CFStringRef const dataUTIStringRef = (__bridge CFStringRef _Nonnull)(dataUTI); - CFStringRef const mimeType = UTTypeCopyPreferredTagWithClass(dataUTIStringRef, kUTTagClassMIMEType); - - NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL - MIMEType:(__bridge NSString *)(mimeType) - expectedContentLength:length - textEncodingName:nil]; - CFRelease(mimeType); - - [delegate URLRequest:cancellationBlock didReceiveResponse:response]; - - [delegate URLRequest:cancellationBlock didReceiveData:imageData]; - [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; + if (asset) { + + ALAssetRepresentation *representation = [asset defaultRepresentation]; + NSInteger length = (NSInteger)representation.size; + CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(representation.UTI), kUTTagClassMIMEType); + + NSURLResponse *response = + [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:(__bridge NSString *)(MIMEType) + expectedContentLength:length + textEncodingName:nil]; + + [delegate URLRequest:cancellationBlock didReceiveResponse:response]; + + NSError *error = nil; + uint8_t *buffer = (uint8_t *)malloc((size_t)length); + if ([representation getBytes:buffer + fromOffset:0 + length:length + error:&error]) { + + NSData *data = [[NSData alloc] initWithBytesNoCopy:buffer + length:length + freeWhenDone:YES]; + + [delegate URLRequest:cancellationBlock didReceiveData:data]; + [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; + + } else { + free(buffer); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + } + + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" + " at URL %@ with no error message.", request.URL]; + NSError *error = RCTErrorWithMessage(errorMessage); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + } + } failureBlock:^(NSError *loadError) { + if (atomic_load(&cancelled)) { + return; + } + [delegate URLRequest:cancellationBlock didCompleteWithError:loadError]; }]; - + return cancellationBlock; } @@ -118,3 +116,12 @@ - (void)cancelRequest:(id)requestToken } @end + +@implementation RCTBridge (RCTAssetsLibraryImageLoader) + +- (ALAssetsLibrary *)assetsLibrary +{ + return [[self moduleForClass:[RCTAssetsLibraryRequestHandler class]] assetsLibrary]; +} + +@end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h index b5d25a04d1eebd..3341c539b0e9dc 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.h +++ b/Libraries/CameraRoll/RCTCameraRollManager.h @@ -5,18 +5,18 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import #import #import -@interface RCTConvert (PHFetchOptions) +@interface RCTConvert (ALAssetGroup) -+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType; ++ (ALAssetsGroupType)ALAssetsGroupType:(id)json; ++ (ALAssetsFilter *)ALAssetsFilter:(id)json; @end - @interface RCTCameraRollManager : NSObject @end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index da4e6f54c49e2c..96e117c16a3783 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -13,7 +13,6 @@ #import #import #import -#import #import #import @@ -23,46 +22,85 @@ #import "RCTAssetsLibraryRequestHandler.h" -@implementation RCTConvert (PHAssetCollectionSubtype) +@implementation RCTConvert (ALAssetGroup) -RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ - @"album": @(PHAssetCollectionSubtypeAny), - @"all": @(PHAssetCollectionSubtypeAny), - @"event": @(PHAssetCollectionSubtypeAlbumSyncedEvent), - @"faces": @(PHAssetCollectionSubtypeAlbumSyncedFaces), - @"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary), - @"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy - @"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), - @"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy - @"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype -}), PHAssetCollectionSubtypeAny, integerValue) +RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{ + // New values + @"album": @(ALAssetsGroupAlbum), + @"all": @(ALAssetsGroupAll), + @"event": @(ALAssetsGroupEvent), + @"faces": @(ALAssetsGroupFaces), + @"library": @(ALAssetsGroupLibrary), + @"photo-stream": @(ALAssetsGroupPhotoStream), + @"saved-photos": @(ALAssetsGroupSavedPhotos), -@end + // Legacy values + @"Album": @(ALAssetsGroupAlbum), + @"All": @(ALAssetsGroupAll), + @"Event": @(ALAssetsGroupEvent), + @"Faces": @(ALAssetsGroupFaces), + @"Library": @(ALAssetsGroupLibrary), + @"PhotoStream": @(ALAssetsGroupPhotoStream), + @"SavedPhotos": @(ALAssetsGroupSavedPhotos), -@implementation RCTConvert (PHFetchOptions) +}), ALAssetsGroupSavedPhotos, integerValue) -+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType +static Class _ALAssetsFilter = nil; +static NSString *_ALAssetsGroupPropertyName = nil; +static NSString *_ALAssetPropertyAssetURL = nil; +static NSString *_ALAssetPropertyLocation = nil; +static NSString *_ALAssetPropertyDate = nil; +static NSString *_ALAssetPropertyType = nil; +static NSString *_ALAssetPropertyDuration = nil; +static NSString *_ALAssetTypeVideo = nil; +static NSString *lookupNSString(void * handle, const char * name) { - // This is not exhaustive in terms of supported media type predicates; more can be added in the future - NSString *const lowercase = [mediaType lowercaseString]; - - if ([lowercase isEqualToString:@"photos"]) { - PHFetchOptions *const options = [PHFetchOptions new]; - options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeImage]; - return options; - } else if ([lowercase isEqualToString:@"videos"]) { - PHFetchOptions *const options = [PHFetchOptions new]; - options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeVideo]; - return options; - } else { - if (![lowercase isEqualToString:@"all"]) { - RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," - "'videos' or 'all'.", mediaType); - } - // This case includes the "all" mediatype - return nil; + void ** sym = dlsym(handle, name); + return (__bridge NSString *)(sym ? *sym : nil); +} +static void ensureAssetsLibLoaded(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); + RCTAssert(handle != NULL, @"Unable to load AssetsLibrary.framework."); + _ALAssetsFilter = objc_getClass("ALAssetsFilter"); + _ALAssetsGroupPropertyName = lookupNSString(handle, "ALAssetsGroupPropertyName"); + _ALAssetPropertyAssetURL = lookupNSString(handle, "ALAssetPropertyAssetURL"); + _ALAssetPropertyLocation = lookupNSString(handle, "ALAssetPropertyLocation"); + _ALAssetPropertyDate = lookupNSString(handle, "ALAssetPropertyDate"); + _ALAssetPropertyType = lookupNSString(handle, "ALAssetPropertyType"); + _ALAssetPropertyDuration = lookupNSString(handle, "ALAssetPropertyDuration"); + _ALAssetTypeVideo = lookupNSString(handle, "ALAssetTypeVideo"); + }); +} + ++ (ALAssetsFilter *)ALAssetsFilter:(id)json +{ + static NSDictionary *options; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ensureAssetsLibLoaded(); + options = @{ + // New values + @"photos": [_ALAssetsFilter allPhotos], + @"videos": [_ALAssetsFilter allVideos], + @"all": [_ALAssetsFilter allAssets], + + // Legacy values + @"Photos": [_ALAssetsFilter allPhotos], + @"Videos": [_ALAssetsFilter allVideos], + @"All": [_ALAssetsFilter allAssets], + }; + }); + + ALAssetsFilter *filter = options[json ?: @"photos"]; + if (!filter) { + RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," + "'videos' or 'all'.", json); } + return filter ?: [_ALAssetsFilter allPhotos]; } @end @@ -73,61 +111,43 @@ @implementation RCTCameraRollManager @synthesize bridge = _bridge; -static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE"; static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; +static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE"; RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request type:(NSString *)type resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - __block PHObjectPlaceholder *placeholder; - - // We load images and videos differently. - // Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary - // URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support - // more ways of loading videos in the future. - __block NSURL *inputURI = nil; - __block UIImage *inputImage = nil; - - void (^saveBlock)(void) = ^void() { - // performChanges and the completionHandler are called on - // arbitrary threads, not the main thread - this is safe - // for now since all JS is queued and executed on a single thread. - // We should reevaluate this if that assumption changes. - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - PHAssetChangeRequest *changeRequest; - - // Defaults to "photo". `type` is an optional param. - if ([type isEqualToString:@"video"]) { - changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI]; - } else { - changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:inputImage]; - } - - placeholder = [changeRequest placeholderForCreatedAsset]; - } completionHandler:^(BOOL success, NSError * _Nullable error) { - if (success) { - NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]]; - resolve(uri); - } else { - reject(kErrorUnableToSave, nil, error); - } - }]; - }; - if ([type isEqualToString:@"video"]) { - inputURI = request.URL; - saveBlock(); + // It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) { + if (saveError) { + reject(kErrorUnableToSave, nil, saveError); + } else { + resolve(assetURL.absoluteString); + } + }]; + }); } else { - [_bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { - if (error) { - reject(kErrorUnableToLoad, nil, error); + [_bridge.imageLoader loadImageWithURLRequest:request + callback:^(NSError *loadError, UIImage *loadedImage) { + if (loadError) { + reject(kErrorUnableToLoad, nil, loadError); return; } - - inputImage = image; - saveBlock(); + // It's unclear if writeImageToSavedPhotosAlbum is thread-safe + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + if (saveError) { + RCTLogWarn(@"Error saving cropped image: %@", saveError); + reject(kErrorUnableToSave, nil, saveError); + } else { + resolve(assetURL.absoluteString); + } + }]; + }); }]; } } @@ -161,131 +181,89 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve, { checkPhotoLibraryConfig(); - NSUInteger const first = [RCTConvert NSInteger:params[@"first"]]; - NSString *const afterCursor = [RCTConvert NSString:params[@"after"]]; - NSString *const groupName = [RCTConvert NSString:params[@"groupName"]]; - NSString *const groupTypes = [[RCTConvert NSString:params[@"groupTypes"]] lowercaseString]; - NSString *const mediaType = [RCTConvert NSString:params[@"assetType"]]; - NSArray *const mimeTypes = [RCTConvert NSStringArray:params[@"mimeTypes"]]; - - // If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all - // other groupTypes values require the "album" collection type. - PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"] - ? PHAssetCollectionTypeSmartAlbum - : PHAssetCollectionTypeAlbum); - PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes]; - - // Predicate for fetching assets within a collection - PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType]; - assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; + ensureAssetsLibLoaded(); + NSUInteger first = [RCTConvert NSInteger:params[@"first"]]; + NSString *afterCursor = [RCTConvert NSString:params[@"after"]]; + NSString *groupName = [RCTConvert NSString:params[@"groupName"]]; + ALAssetsFilter *assetType = [RCTConvert ALAssetsFilter:params[@"assetType"]]; + ALAssetsGroupType groupTypes = [RCTConvert ALAssetsGroupType:params[@"groupTypes"]]; BOOL __block foundAfter = NO; BOOL __block hasNextPage = NO; BOOL __block resolvedPromise = NO; NSMutableArray *> *assets = [NSMutableArray new]; - - // Filter collection name ("group") - PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new]; - collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]]; - if (groupName != nil) { - collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"localizedTitle == '%@'", groupName]]; - } - - PHFetchResult *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions]; - [assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) { - // Enumerate assets within the collection - PHFetchResult *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions]; - - [assetsFetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) { - NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]]; - if (afterCursor && !foundAfter) { - if ([afterCursor isEqualToString:uri]) { - foundAfter = YES; - } - return; // skip until we get to the first one - } - - // Get underlying resources of an asset - this includes files as well as details about edited PHAssets - if ([mimeTypes count] > 0) { - NSArray *const assetResources = [PHAssetResource assetResourcesForAsset:asset]; - if (![assetResources firstObject]) { - return; - } - - PHAssetResource *const _Nonnull resource = [assetResources firstObject]; - CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier); - NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)); - - BOOL __block mimeTypeFound = NO; - [mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) { - if ([mimeType isEqualToString:mimeTypeFilter]) { - mimeTypeFound = YES; - *stop = YES; + + [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { + if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:_ALAssetsGroupPropertyName]])) { + + [group setAssetsFilter:assetType]; + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { + if (result) { + NSString *uri = ((NSURL *)[result valueForProperty:_ALAssetPropertyAssetURL]).absoluteString; + if (afterCursor && !foundAfter) { + if ([afterCursor isEqualToString:uri]) { + foundAfter = YES; + } + return; // Skip until we get to the first one } - }]; - - if (!mimeTypeFound) { - return; + if (first == assets.count) { + *stopAssets = YES; + *stopGroups = YES; + hasNextPage = YES; + RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); + RCTResolvePromise(resolve, assets, hasNextPage); + resolvedPromise = YES; + return; + } + CGSize dimensions = [result defaultRepresentation].dimensions; + CLLocation *loc = [result valueForProperty:_ALAssetPropertyLocation]; + NSDate *date = [result valueForProperty:_ALAssetPropertyDate]; + NSString *filename = [result defaultRepresentation].filename; + int64_t duration = 0; + if ([[result valueForProperty:_ALAssetPropertyType] isEqualToString:_ALAssetTypeVideo]) { + duration = [[result valueForProperty:_ALAssetPropertyDuration] intValue]; + } + + [assets addObject:@{ + @"node": @{ + @"type": [result valueForProperty:_ALAssetPropertyType], + @"group_name": [group valueForProperty:_ALAssetsGroupPropertyName], + @"image": @{ + @"uri": uri, + @"filename" : filename ?: [NSNull null], + @"height": @(dimensions.height), + @"width": @(dimensions.width), + @"isStored": @YES, + @"playableDuration": @(duration), + }, + @"timestamp": @(date.timeIntervalSince1970), + @"location": loc ? @{ + @"latitude": @(loc.coordinate.latitude), + @"longitude": @(loc.coordinate.longitude), + @"altitude": @(loc.altitude), + @"heading": @(loc.course), + @"speed": @(loc.speed), + } : @{}, + } + }]; } - } - - // If we've accumulated enough results to resolve a single promise - if (first == assets.count) { - *stopAssets = YES; - *stopCollections = YES; - hasNextPage = YES; - RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); + }]; + } + + if (!group) { + // Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise + // multiple times here. + if (!resolvedPromise) { RCTResolvePromise(resolve, assets, hasNextPage); resolvedPromise = YES; - return; } - - NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo - ? @"video" - : (asset.mediaType == PHAssetMediaTypeImage - ? @"image" - : (asset.mediaType == PHAssetMediaTypeAudio - ? @"audio" - : @"unknown"))); - CLLocation *const loc = asset.location; - - // A note on isStored: in the previous code that used ALAssets, isStored - // was always set to YES, probably because iCloud-synced images were never returned (?). - // To get the "isStored" information and filename, we would need to actually request the - // image data from the image manager. Those operations could get really expensive and - // would definitely utilize the disk too much. - // Thus, this field is actually not reliable. - // Note that Android also does not return the `isStored` field at all. - [assets addObject:@{ - @"node": @{ - @"type": assetMediaTypeLabel, // TODO: switch to mimeType? - @"group_name": [assetCollection localizedTitle], - @"image": @{ - @"uri": uri, - @"height": @([asset pixelHeight]), - @"width": @([asset pixelWidth]), - @"isStored": @YES, // this field doesn't seem to exist on android - @"playableDuration": @([asset duration]) // fractional seconds - }, - @"timestamp": @(asset.creationDate.timeIntervalSince1970), - @"location": (loc ? @{ - @"latitude": @(loc.coordinate.latitude), - @"longitude": @(loc.coordinate.longitude), - @"altitude": @(loc.altitude), - @"heading": @(loc.course), - @"speed": @(loc.speed), // speed in m/s - } : @{}) - } - }]; - }]; + } + } failureBlock:^(NSError *error) { + if (error.code != ALAssetsLibraryAccessUserDeniedError) { + RCTLogError(@"Failure while iterating through asset groups %@", error); + } + reject(kErrorUnableToLoad, nil, error); }]; - - // If we get this far and haven't resolved the promise yet, we reached the end of the list of photos - if (!resolvedPromise) { - hasNextPage = NO; - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; - } } RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m index ccdf3c27f73087..e6c191e93b5099 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m @@ -39,7 +39,7 @@ - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL // Using PhotoKit for iOS 8+ // The 'ph://' prefix is used by FBMediaKit to differentiate between // assets-library. It is prepended to the local ID so that it is in the - // form of an NSURL which is what assets-library uses. + // form of an, NSURL which is what assets-library uses. NSString *assetID = @""; PHFetchResult *results; if (!imageURL) { diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 6ceb7da4fa1650..d296637c1fe18e 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -236,7 +236,7 @@ - (void)setImageCache:(id)cache return image; } -- (RCTImageLoaderCancellationBlock) loadImageWithURLRequest:(NSURLRequest *)imageURLRequest +- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithURLRequest:imageURLRequest