2015-03-23 22:07:33 +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.
|
|
|
|
*/
|
2015-03-11 02:11:28 +00:00
|
|
|
|
|
|
|
#import "RCTCameraRollManager.h"
|
|
|
|
|
|
|
|
#import <CoreLocation/CoreLocation.h>
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
|
2016-11-23 15:47:52 +00:00
|
|
|
#import <React/RCTBridge.h>
|
|
|
|
#import <React/RCTConvert.h>
|
|
|
|
#import <React/RCTImageLoader.h>
|
|
|
|
#import <React/RCTLog.h>
|
|
|
|
#import <React/RCTUtils.h>
|
|
|
|
|
2016-01-20 19:03:22 +00:00
|
|
|
#import "RCTAssetsLibraryRequestHandler.h"
|
2015-03-11 02:11:28 +00:00
|
|
|
|
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-03-11 02:11:28 +00:00
|
|
|
@implementation RCTCameraRollManager
|
|
|
|
|
2015-04-09 15:46:53 +00:00
|
|
|
RCT_EXPORT_MODULE()
|
2015-03-11 02:11:28 +00:00
|
|
|
|
2015-07-21 05:44:42 +00:00
|
|
|
@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-04-09 15:46:53 +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-03-11 02:11:28 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 16:07:01 +00:00
|
|
|
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
|
|
|
NSArray<NSDictionary<NSString *, id> *> *assets,
|
|
|
|
BOOL hasNextPage)
|
2015-03-11 02:11:28 +00:00
|
|
|
{
|
2015-08-24 10:14:33 +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-03-11 02:11:28 +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-03-11 02:11:28 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 15:46:53 +00:00
|
|
|
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
2016-01-21 16:07:01 +00:00
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
2015-03-11 02:11:28 +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-03-11 02:11:28 +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-03-11 02:11:28 +00:00
|
|
|
|
2015-07-27 15:48:31 +00:00
|
|
|
[_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) {
|
2015-03-11 02:11:28 +00:00
|
|
|
if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) {
|
2015-06-01 22:46:06 +00:00
|
|
|
|
2015-11-03 22:45:46 +00:00
|
|
|
[group setAssetsFilter:assetType];
|
2015-03-11 02:11:28 +00:00
|
|
|
[group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) {
|
|
|
|
if (result) {
|
2015-08-24 10:14:33 +00:00
|
|
|
NSString *uri = ((NSURL *)[result valueForProperty:ALAssetPropertyAssetURL]).absoluteString;
|
2015-03-11 02:11:28 +00:00
|
|
|
if (afterCursor && !foundAfter) {
|
|
|
|
if ([afterCursor isEqualToString:uri]) {
|
|
|
|
foundAfter = YES;
|
|
|
|
}
|
|
|
|
return; // Skip until we get to the first one
|
|
|
|
}
|
2015-08-24 10:14:33 +00:00
|
|
|
if (first == assets.count) {
|
2015-03-11 02:11:28 +00:00
|
|
|
*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-03-11 02:11:28 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
CGSize dimensions = [result defaultRepresentation].dimensions;
|
|
|
|
CLLocation *loc = [result valueForProperty:ALAssetPropertyLocation];
|
|
|
|
NSDate *date = [result valueForProperty:ALAssetPropertyDate];
|
2016-09-18 03:29:27 +00:00
|
|
|
NSString *filename = [result defaultRepresentation].filename;
|
2017-07-15 00:33:52 +00:00
|
|
|
int64_t duration = 0;
|
|
|
|
if ([[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypeVideo]) {
|
|
|
|
duration = [[result valueForProperty:ALAssetPropertyDuration] intValue];
|
|
|
|
}
|
|
|
|
|
2015-03-11 02:11:28 +00:00
|
|
|
[assets addObject:@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"node": @{
|
|
|
|
@"type": [result valueForProperty:ALAssetPropertyType],
|
|
|
|
@"group_name": [group valueForProperty:ALAssetsGroupPropertyName],
|
|
|
|
@"image": @{
|
|
|
|
@"uri": uri,
|
2016-09-18 03:29:27 +00:00
|
|
|
@"filename" : filename,
|
2015-11-14 18:25:00 +00:00
|
|
|
@"height": @(dimensions.height),
|
|
|
|
@"width": @(dimensions.width),
|
|
|
|
@"isStored": @YES,
|
2017-07-15 00:33:52 +00:00
|
|
|
@"playableDuration": @(duration),
|
2015-11-14 18:25:00 +00:00
|
|
|
},
|
|
|
|
@"timestamp": @(date.timeIntervalSince1970),
|
|
|
|
@"location": loc ? @{
|
|
|
|
@"latitude": @(loc.coordinate.latitude),
|
|
|
|
@"longitude": @(loc.coordinate.longitude),
|
|
|
|
@"altitude": @(loc.altitude),
|
|
|
|
@"heading": @(loc.course),
|
|
|
|
@"speed": @(loc.speed),
|
|
|
|
} : @{},
|
|
|
|
}
|
|
|
|
}];
|
2015-03-11 02:11:28 +00:00
|
|
|
}
|
|
|
|
}];
|
2016-11-23 15:47:52 +00:00
|
|
|
}
|
|
|
|
|
iOS: Fix CameraRoll to support custom user groups
Summary:
on iOS, if you pull photo from one of user's custom album, the app crashes on the assertion `RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");` . assertion that was assumed to never been reached.
According to iOS doc, the enumerateGroupsWithTypes `usingBlock` block is called with `group=nil` when the iteration is over, but in current react-native implementation, it is stopping in other circumstance (because the `else` case) which is probably a mistake.
You have probably never seen the bug because you didn't tried to use getPhotos with something else than the pre-defined groups, but it should be possible to do so *(and it seems to work fine as soon as I included my fix. Later I should provide a PR that includes a way to list user groups :) but at least I need this to gets in, otherwise it crashes)*.
For instance, User have a Photo Folder (or "album", whatever you call it) called "Instagram", when I call `CameraRoll.getPhotos({ groupName: "Instagram",
Closes https://github.com/facebook/react-native/pull/10272
Differential Revision: D4009342
Pulled By: javache
fbshipit-source-id: a73ca828133b4f0d880c229f9b675538854020de
2016-10-12 18:18:58 +00:00
|
|
|
if (!group) {
|
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-03-11 02:11:28 +00:00
|
|
|
// multiple times here.
|
2016-01-21 16:07:01 +00:00
|
|
|
if (!resolvedPromise) {
|
|
|
|
RCTResolvePromise(resolve, assets, hasNextPage);
|
|
|
|
resolvedPromise = YES;
|
2015-03-11 02:11:28 +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-03-11 02:11:28 +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-03-11 02:11:28 +00:00
|
|
|
@end
|