diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index a4f0d7fd6..4b7f23f3e 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -115,33 +115,45 @@ class CameraRoll { static GroupTypesOptions: Array; static AssetTypeOptions: Array; + + 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"`. + * On Android, the tag must be a local image or video URI, such as `"file:///sdcard/img.png"`. * - * On iOS, the tag can be one of the following: + * On iOS, the tag can be any image URI (including local, remote asset-library and base64 data URIs) + * or a local video file URI (remote or data URIs are not supported for saving video at this time). * - * - local URI - * - assets-library tag - * - 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) + * 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 when resolved will be passed the new URI. + * 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); } /** diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index 7d9e82aa2..8512cf64e 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -82,28 +82,42 @@ RCT_EXPORT_MODULE() NSString *const RCTErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; NSString *const RCTErrorUnableToSave = @"E_UNABLE_TO_SAVE"; -RCT_EXPORT_METHOD(saveImageWithTag:(NSURLRequest *)imageRequest +RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request + 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:@"video"]) { + // It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe dispatch_async(dispatch_get_main_queue(), ^{ - [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + [_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) { if (saveError) { - RCTLogWarn(@"Error saving cropped image: %@", saveError); reject(RCTErrorUnableToSave, nil, saveError); } else { resolve(assetURL.absoluteString); } }]; }); - }]; + } 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(), ^{ + [_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); + } + }]; + }); + }]; + } } static void RCTResolvePromise(RCTPromiseResolveBlock resolve, diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java index e5302355c..c103f445a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/camera/CameraRollManager.java @@ -117,22 +117,26 @@ public class CameraRollManager extends ReactContextBaseJavaModule { * @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 { + private enum MediaType { PHOTO, VIDEO }; + private static class SaveToCameraRoll extends GuardedAsyncTask { 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 @@ -140,14 +144,15 @@ public class CameraRollManager extends ReactContextBaseJavaModule { 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; @@ -159,7 +164,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule { 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();