Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions Libraries/CameraRoll/CameraRoll.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,14 @@ class CameraRoll {

static GroupTypesOptions: Array<string>;
static AssetTypeOptions: Array<string>;

static saveImageWithTag(tag: string):Promise<*> {
console.warn('CameraRoll.saveImageWithTag is deprecated. Use CameraRoll.saveToCameraRoll instead');
return this.saveToCameraRoll(tag, 'photo');
}

/**
* Saves the image to the camera roll / gallery.
* Saves the photo or video to the camera roll / gallery.
*
* On Android, the tag is a local URI, such as `"file:///sdcard/img.png"`.
*
Expand All @@ -127,21 +133,31 @@ class CameraRoll {
* - a tag not matching any of the above, which means the image data will
* be stored in memory (and consume memory as long as the process is alive)
*
* Returns a Promise which when resolved will be passed the new URI.
* If the tag has a file extension of .mov or .mp4, it will be inferred as a video. Otherwise
* it will be treated as a photo. To override the automatic choice, you can pass an optional
* `type` parameter that must be one of 'photo' or 'video'.
*
* Returns a Promise which will resolve with the new URI.
*/
static saveImageWithTag(tag) {
static saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<*> {
invariant(
typeof tag === 'string',
'CameraRoll.saveImageWithTag tag must be a valid string.'
'CameraRoll.saveToCameraRoll must be a valid string.'
);
if (arguments.length > 1) {
console.warn('CameraRoll.saveImageWithTag(tag, success, error) is deprecated. Use the returned Promise instead');
const successCallback = arguments[1];
const errorCallback = arguments[2] || ( () => {} );
RCTCameraRollManager.saveImageWithTag(tag).then(successCallback, errorCallback);
return;

invariant(
type === 'photo' || type === 'video' || type === undefined,
`The second argument to saveToCameraRoll must be 'photo' or 'video'. You passed ${type}`
);

let mediaType = 'photo';
if (type) {
mediaType = type;
} else if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
mediaType = 'video';
}
return RCTCameraRollManager.saveImageWithTag(tag);

return RCTCameraRollManager.saveToCameraRoll(tag, mediaType);
}

/**
Expand Down
38 changes: 27 additions & 11 deletions Libraries/CameraRoll/RCTCameraRollManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,44 @@ @implementation RCTCameraRollManager
NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE";

RCT_EXPORT_METHOD(saveImageWithTag:(NSURLRequest *)imageRequest
RCT_EXPORT_METHOD(saveToCameraRoll:(NSString *)tag
type:(NSString *)type
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[_bridge.imageLoader loadImageWithURLRequest:imageRequest
callback:^(NSError *loadError, UIImage *loadedImage) {
if (loadError) {
reject(RCTErrorUnableToLoad, nil, loadError);
return;
}
// It's unclear if writeImageToSavedPhotosAlbum is thread-safe
if ([type isEqualToString:@"photo"]) {
[_bridge.imageLoader loadImageWithURLRequest:[RCTConvert NSURLRequest:tag]
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(), ^{
[_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
if (saveError) {
RCTLogWarn(@"Error saving cropped image: %@", saveError);
reject(RCTErrorUnableToSave, nil, saveError);
} else {
resolve(assetURL.absoluteString);
}
}];
});
}];
}
else if ([type isEqualToString:@"video"]) {
dispatch_async(dispatch_get_main_queue(), ^{
[_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
[_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:[NSURL URLWithString:tag] completionBlock:^(NSURL *assetURL, NSError *saveError) {
if (saveError) {
RCTLogWarn(@"Error saving cropped image: %@", saveError);
reject(RCTErrorUnableToSave, nil, saveError);
} else {
resolve(assetURL.absoluteString);
}
}];
});
}];
} else {
reject(RCTErrorUnspecified, [@"unexpected type " stringByAppendingString:type], nil);
}
}

static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,37 +117,42 @@ public Map<String, Object> getConstants() {
* @param promise to be resolved or rejected
*/
@ReactMethod
public void saveImageWithTag(String uri, Promise promise) {
new SaveImageTag(getReactApplicationContext(), Uri.parse(uri), promise)
public void saveToCameraRoll(String uri, String type, Promise promise) {
MediaType parsedType = type.equals("video") ? MediaType.VIDEO : MediaType.PHOTO;
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), parsedType, promise)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

private static class SaveImageTag extends GuardedAsyncTask<Void, Void> {
private enum MediaType { PHOTO, VIDEO };
private static class SaveToCameraRoll extends GuardedAsyncTask<Void, Void> {

private final Context mContext;
private final Uri mUri;
private final Promise mPromise;
private final MediaType mType;

public SaveImageTag(ReactContext context, Uri uri, Promise promise) {
public SaveToCameraRoll(ReactContext context, Uri uri, MediaType type, Promise promise) {
super(context);
mContext = context;
mUri = uri;
mPromise = promise;
mType = type;
}

@Override
protected void doInBackgroundGuarded(Void... params) {
File source = new File(mUri.getPath());
FileChannel input = null, output = null;
try {
File pictures =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
pictures.mkdirs();
if (!pictures.isDirectory()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available");
File exportDir = (mType == MediaType.PHOTO)
? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
exportDir.mkdirs();
if (!exportDir.isDirectory()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
return;
}
File dest = new File(pictures, source.getName());
File dest = new File(exportDir, source.getName());
int n = 0;
String fullSourceName = source.getName();
String sourceName, sourceExt;
Expand All @@ -159,7 +164,7 @@ protected void doInBackgroundGuarded(Void... params) {
sourceExt = "";
}
while (!dest.createNewFile()) {
dest = new File(pictures, sourceName + "_" + (n++) + sourceExt);
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
}
input = new FileInputStream(source).getChannel();
output = new FileOutputStream(dest).getChannel();
Expand Down