2015-09-08 15:58:13 +00:00
|
|
|
/**
|
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the BSD-style license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#import "RCTCameraRollManager.h"
|
|
|
|
|
|
|
|
#import <CoreLocation/CoreLocation.h>
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
|
2016-01-20 19:03:22 +00:00
|
|
|
#import "RCTAssetsLibraryRequestHandler.h"
|
2015-09-08 15:58:13 +00:00
|
|
|
#import "RCTBridge.h"
|
2015-11-03 22:45:46 +00:00
|
|
|
#import "RCTConvert.h"
|
2015-09-08 15:58:13 +00:00
|
|
|
#import "RCTImageLoader.h"
|
|
|
|
#import "RCTLog.h"
|
|
|
|
#import "RCTUtils.h"
|
|
|
|
|
2015-11-03 22:45:46 +00:00
|
|
|
@implementation RCTConvert (ALAssetGroup)
|
|
|
|
|
|
|
|
RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{
|
|
|
|
|
|
|
|
// 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),
|
|
|
|
|
|
|
|
}), ALAssetsGroupSavedPhotos, integerValue)
|
|
|
|
|
|
|
|
+ (ALAssetsFilter *)ALAssetsFilter:(id)json
|
|
|
|
{
|
2015-11-14 18:25:00 +00:00
|
|
|
static NSDictionary<NSString *, ALAssetsFilter *> *options;
|
2015-11-03 22:45:46 +00:00
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
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
|
|
|
|
|
2015-09-08 15:58:13 +00:00
|
|
|
@implementation RCTCameraRollManager
|
|
|
|
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
|
2016-01-21 16:07:01 +00:00
|
|
|
NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
|
|
|
|
NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE";
|
|
|
|
|
Allow CameraRoll to export videos
Summary:
This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows:
```
// old
saveImageWithTag(tag: string): Promise<string>
// new
saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>
```
if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed.
I've left in the `saveImageWithTag` method for now with a deprecation warning.
**Test plan (required)**
I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms.
```js
// index.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
CameraRoll,
} from 'react-native';
import FS fro
Closes https://github.com/facebook/react-native/pull/7988
Differential Revision: D3401251
Pulled By: nicklockwood
fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
2016-06-07 23:37:48 +00:00
|
|
|
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
|
|
|
type:(NSString *)type
|
2016-01-21 16:07:01 +00:00
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
Allow CameraRoll to export videos
Summary:
This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows:
```
// old
saveImageWithTag(tag: string): Promise<string>
// new
saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>
```
if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed.
I've left in the `saveImageWithTag` method for now with a deprecation warning.
**Test plan (required)**
I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms.
```js
// index.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
CameraRoll,
} from 'react-native';
import FS fro
Closes https://github.com/facebook/react-native/pull/7988
Differential Revision: D3401251
Pulled By: nicklockwood
fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
2016-06-07 23:37:48 +00:00
|
|
|
if ([type isEqualToString:@"video"]) {
|
|
|
|
// It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe
|
2015-10-20 12:00:50 +00:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2016-07-07 19:36:56 +00:00
|
|
|
[self->_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
2015-10-20 12:00:50 +00:00
|
|
|
if (saveError) {
|
2016-01-21 16:07:01 +00:00
|
|
|
reject(RCTErrorUnableToSave, nil, saveError);
|
2015-10-20 12:00:50 +00:00
|
|
|
} else {
|
2016-02-10 15:24:38 +00:00
|
|
|
resolve(assetURL.absoluteString);
|
2015-10-20 12:00:50 +00:00
|
|
|
}
|
|
|
|
}];
|
|
|
|
});
|
Allow CameraRoll to export videos
Summary:
This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows:
```
// old
saveImageWithTag(tag: string): Promise<string>
// new
saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>
```
if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed.
I've left in the `saveImageWithTag` method for now with a deprecation warning.
**Test plan (required)**
I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms.
```js
// index.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
CameraRoll,
} from 'react-native';
import FS fro
Closes https://github.com/facebook/react-native/pull/7988
Differential Revision: D3401251
Pulled By: nicklockwood
fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
2016-06-07 23:37:48 +00:00
|
|
|
} else {
|
|
|
|
[_bridge.imageLoader loadImageWithURLRequest:request
|
|
|
|
callback:^(NSError *loadError, UIImage *loadedImage) {
|
|
|
|
if (loadError) {
|
|
|
|
reject(RCTErrorUnableToLoad, nil, loadError);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// It's unclear if writeImageToSavedPhotosAlbum is thread-safe
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2016-07-07 19:36:56 +00:00
|
|
|
[self->_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
Allow CameraRoll to export videos
Summary:
This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows:
```
// old
saveImageWithTag(tag: string): Promise<string>
// new
saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>
```
if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed.
I've left in the `saveImageWithTag` method for now with a deprecation warning.
**Test plan (required)**
I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms.
```js
// index.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
CameraRoll,
} from 'react-native';
import FS fro
Closes https://github.com/facebook/react-native/pull/7988
Differential Revision: D3401251
Pulled By: nicklockwood
fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
2016-06-07 23:37:48 +00:00
|
|
|
if (saveError) {
|
|
|
|
RCTLogWarn(@"Error saving cropped image: %@", saveError);
|
|
|
|
reject(RCTErrorUnableToSave, nil, saveError);
|
|
|
|
} else {
|
|
|
|
resolve(assetURL.absoluteString);
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
});
|
|
|
|
}];
|
|
|
|
}
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 16:07:01 +00:00
|
|
|
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
|
|
|
NSArray<NSDictionary<NSString *, id> *> *assets,
|
|
|
|
BOOL hasNextPage)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
|
|
|
if (!assets.count) {
|
2016-02-10 15:24:38 +00:00
|
|
|
resolve(@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"edges": assets,
|
|
|
|
@"page_info": @{
|
|
|
|
@"has_next_page": @NO,
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
});
|
2015-09-08 15:58:13 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
resolve(@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"edges": assets,
|
|
|
|
@"page_info": @{
|
|
|
|
@"start_cursor": assets[0][@"node"][@"image"][@"uri"],
|
|
|
|
@"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"],
|
|
|
|
@"has_next_page": @(hasNextPage),
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
});
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
2016-01-21 16:07:01 +00:00
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
2016-08-18 14:16:26 +00:00
|
|
|
checkPhotoLibraryConfig();
|
|
|
|
|
2015-11-03 22:45:46 +00:00
|
|
|
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"]];
|
2015-09-08 15:58:13 +00:00
|
|
|
|
|
|
|
BOOL __block foundAfter = NO;
|
|
|
|
BOOL __block hasNextPage = NO;
|
2016-01-21 16:07:01 +00:00
|
|
|
BOOL __block resolvedPromise = NO;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
|
2015-09-08 15:58:13 +00:00
|
|
|
|
|
|
|
[_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) {
|
|
|
|
if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) {
|
|
|
|
|
2015-11-03 22:45:46 +00:00
|
|
|
[group setAssetsFilter:assetType];
|
2015-09-08 15:58:13 +00:00
|
|
|
[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 (first == assets.count) {
|
|
|
|
*stopAssets = YES;
|
|
|
|
*stopGroups = YES;
|
|
|
|
hasNextPage = YES;
|
2016-01-21 16:07:01 +00:00
|
|
|
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
|
|
|
|
RCTResolvePromise(resolve, assets, hasNextPage);
|
|
|
|
resolvedPromise = YES;
|
2015-09-08 15:58:13 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
CGSize dimensions = [result defaultRepresentation].dimensions;
|
|
|
|
CLLocation *loc = [result valueForProperty:ALAssetPropertyLocation];
|
|
|
|
NSDate *date = [result valueForProperty:ALAssetPropertyDate];
|
|
|
|
[assets addObject:@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"node": @{
|
|
|
|
@"type": [result valueForProperty:ALAssetPropertyType],
|
|
|
|
@"group_name": [group valueForProperty:ALAssetsGroupPropertyName],
|
|
|
|
@"image": @{
|
|
|
|
@"uri": uri,
|
|
|
|
@"height": @(dimensions.height),
|
|
|
|
@"width": @(dimensions.width),
|
|
|
|
@"isStored": @YES,
|
|
|
|
},
|
|
|
|
@"timestamp": @(date.timeIntervalSince1970),
|
|
|
|
@"location": loc ? @{
|
|
|
|
@"latitude": @(loc.coordinate.latitude),
|
|
|
|
@"longitude": @(loc.coordinate.longitude),
|
|
|
|
@"altitude": @(loc.altitude),
|
|
|
|
@"heading": @(loc.course),
|
|
|
|
@"speed": @(loc.speed),
|
|
|
|
} : @{},
|
|
|
|
}
|
|
|
|
}];
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
}];
|
|
|
|
} else {
|
2016-01-21 16:07:01 +00:00
|
|
|
// Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise
|
2015-09-08 15:58:13 +00:00
|
|
|
// multiple times here.
|
2016-01-21 16:07:01 +00:00
|
|
|
if (!resolvedPromise) {
|
|
|
|
RCTResolvePromise(resolve, assets, hasNextPage);
|
|
|
|
resolvedPromise = YES;
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} failureBlock:^(NSError *error) {
|
|
|
|
if (error.code != ALAssetsLibraryAccessUserDeniedError) {
|
|
|
|
RCTLogError(@"Failure while iterating through asset groups %@", error);
|
|
|
|
}
|
2016-01-21 16:07:01 +00:00
|
|
|
reject(RCTErrorUnableToLoad, nil, error);
|
2015-09-08 15:58:13 +00:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
2016-08-18 14:16:26 +00:00
|
|
|
static void checkPhotoLibraryConfig()
|
|
|
|
{
|
|
|
|
#if RCT_DEV
|
|
|
|
if (![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSPhotoLibraryUsageDescription"]) {
|
|
|
|
RCTLogError(@"NSPhotoLibraryUsageDescription key must be present in Info.plist to use camera roll.");
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2015-09-08 15:58:13 +00:00
|
|
|
@end
|