Un-revert D13513777: Replace ALAssets* with PHPhoto* in RCTCameraRoll

Summary: Replaced all deprecated ALAssets* references to roughly equivalent PHPhoto* references in RCTCameraRoll library. There are still some minor inconsistencies between iOS/Android and documentation that existed prior to this diff that need to be resolved after this.

Reviewed By: fkgozali

Differential Revision: D13593314

fbshipit-source-id: 6d3dc43383e3ad6e3dbe73d4ceceac1ba9261d9d
This commit is contained in:
Joshua Gross 2019-01-07 16:11:52 -08:00 committed by Facebook Github Bot
parent ac32e98217
commit e172dc7b94
4 changed files with 263 additions and 257 deletions

View File

@ -8,17 +8,8 @@
#import <React/RCTBridge.h>
#import <React/RCTURLRequestHandler.h>
@class ALAssetsLibrary;
@class PHPhotoLibrary;
@interface RCTAssetsLibraryRequestHandler : NSObject <RCTURLRequestHandler>
@end
@interface RCTBridge (RCTAssetsLibraryImageLoader)
/**
* The shared asset library instance.
*/
@property (nonatomic, readonly) ALAssetsLibrary *assetsLibrary;
@end

View File

@ -11,41 +11,26 @@
#import <dlfcn.h>
#import <objc/runtime.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <Photos/Photos.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <React/RCTBridge.h>
#import <React/RCTUtils.h>
@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
{
return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame;
if (![PHAsset class]) {
return NO;
}
return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame
|| [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame;
}
- (id)sendRequest:(NSURLRequest *)request
@ -56,55 +41,72 @@ static void ensureAssetsLibLoaded(void)
atomic_store(&cancelled, YES);
};
[[self assetsLibrary] assetForURL:request.URL resultBlock:^(ALAsset *asset) {
if (atomic_load(&cancelled)) {
return;
if (!request.URL) {
NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"];
[delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)];
return cancellationBlock;
}
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];
PHFetchResult<PHAsset *> *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 {
free(buffer);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", request.URL];
[delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)];
return cancellationBlock;
}
} else {
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;
}
} failureBlock:^(NSError *loadError) {
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];
return;
}
[delegate URLRequest:cancellationBlock didCompleteWithError:loadError];
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];
}];
return cancellationBlock;
@ -116,12 +118,3 @@ static void ensureAssetsLibLoaded(void)
}
@end
@implementation RCTBridge (RCTAssetsLibraryImageLoader)
- (ALAssetsLibrary *)assetsLibrary
{
return [[self moduleForClass:[RCTAssetsLibraryRequestHandler class]] assetsLibrary];
}
@end

View File

@ -5,18 +5,18 @@
* LICENSE file in the root directory of this source tree.
*/
#import <AssetsLibrary/AssetsLibrary.h>
#import <Photos/Photos.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTConvert.h>
@interface RCTConvert (ALAssetGroup)
@interface RCTConvert (PHFetchOptions)
+ (ALAssetsGroupType)ALAssetsGroupType:(id)json;
+ (ALAssetsFilter *)ALAssetsFilter:(id)json;
+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType;
@end
@interface RCTCameraRollManager : NSObject <RCTBridgeModule>
@end

View File

@ -13,6 +13,7 @@
#import <Photos/Photos.h>
#import <dlfcn.h>
#import <objc/runtime.h>
#import <MobileCoreServices/UTType.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
@ -22,85 +23,46 @@
#import "RCTAssetsLibraryRequestHandler.h"
@implementation RCTConvert (ALAssetGroup)
@implementation RCTConvert (PHAssetCollectionSubtype)
RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{
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)
// New values
@"album": @(ALAssetsGroupAlbum),
@"all": @(ALAssetsGroupAll),
@"event": @(ALAssetsGroupEvent),
@"faces": @(ALAssetsGroupFaces),
@"library": @(ALAssetsGroupLibrary),
@"photo-stream": @(ALAssetsGroupPhotoStream),
@"saved-photos": @(ALAssetsGroupSavedPhotos),
// Legacy values
@"Album": @(ALAssetsGroupAlbum),
@"All": @(ALAssetsGroupAll),
@"Event": @(ALAssetsGroupEvent),
@"Faces": @(ALAssetsGroupFaces),
@"Library": @(ALAssetsGroupLibrary),
@"PhotoStream": @(ALAssetsGroupPhotoStream),
@"SavedPhotos": @(ALAssetsGroupSavedPhotos),
@end
}), ALAssetsGroupSavedPhotos, integerValue)
@implementation RCTConvert (PHFetchOptions)
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)
+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType
{
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");
});
}
// This is not exhaustive in terms of supported media type predicates; more can be added in the future
NSString *const lowercase = [mediaType lowercaseString];
+ (ALAssetsFilter *)ALAssetsFilter:(id)json
{
static NSDictionary<NSString *, ALAssetsFilter *> *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) {
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'.", json);
"'videos' or 'all'.", mediaType);
}
// This case includes the "all" mediatype
return nil;
}
return filter ?: [_ALAssetsFilter allPhotos];
}
@end
@ -111,43 +73,61 @@ RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE";
static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
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"]) {
// 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);
changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI];
} else {
resolve(assetURL.absoluteString);
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();
} else {
[_bridge.imageLoader loadImageWithURLRequest:request
callback:^(NSError *loadError, UIImage *loadedImage) {
if (loadError) {
reject(kErrorUnableToLoad, nil, loadError);
[_bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) {
if (error) {
reject(kErrorUnableToLoad, nil, error);
return;
}
// 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);
}
}];
});
inputImage = image;
saveBlock();
}];
}
}
@ -181,90 +161,132 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
{
checkPhotoLibraryConfig();
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"]];
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<NSString *> *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]];
BOOL __block foundAfter = NO;
BOOL __block hasNextPage = NO;
BOOL __block resolvedPromise = NO;
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
[_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) {
if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:_ALAssetsGroupPropertyName]])) {
// 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]];
}
[group setAssetsFilter:assetType];
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) {
if (result) {
NSString *uri = ((NSURL *)[result valueForProperty:_ALAssetPropertyAssetURL]).absoluteString;
PHFetchResult<PHAssetCollection *> *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<PHAsset *> *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
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<PHAssetResource *> *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;
}
}];
if (!mimeTypeFound) {
return;
}
}
// If we've accumulated enough results to resolve a single promise
if (first == assets.count) {
*stopAssets = YES;
*stopGroups = YES;
*stopCollections = 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];
}
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": [result valueForProperty:_ALAssetPropertyType],
@"group_name": [group valueForProperty:_ALAssetsGroupPropertyName],
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
@"group_name": [assetCollection localizedTitle],
@"image": @{
@"uri": uri,
@"filename" : filename ?: [NSNull null],
@"height": @(dimensions.height),
@"width": @(dimensions.width),
@"isStored": @YES,
@"playableDuration": @(duration),
@"height": @([asset pixelHeight]),
@"width": @([asset pixelWidth]),
@"isStored": @YES, // this field doesn't seem to exist on android
@"playableDuration": @([asset duration]) // fractional seconds
},
@"timestamp": @(date.timeIntervalSince1970),
@"location": loc ? @{
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
@"location": (loc ? @{
@"latitude": @(loc.coordinate.latitude),
@"longitude": @(loc.coordinate.longitude),
@"altitude": @(loc.altitude),
@"heading": @(loc.course),
@"speed": @(loc.speed),
} : @{},
@"speed": @(loc.speed), // speed in m/s
} : @{})
}
}];
}
}];
}
}];
if (!group) {
// Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise
// multiple times here.
// 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;
}
}
} failureBlock:^(NSError *error) {
if (error.code != ALAssetsLibraryAccessUserDeniedError) {
RCTLogError(@"Failure while iterating through asset groups %@", error);
}
reject(kErrorUnableToLoad, nil, error);
}];
}
RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets
resolve:(RCTPromiseResolveBlock)resolve