From 63769518e0c7db60eb39bb5f47fe24f4bc664862 Mon Sep 17 00:00:00 2001 From: Albert Sun Date: Fri, 11 Oct 2019 16:31:17 -0700 Subject: [PATCH] Fix selecting videos from library in iOS 13 Summary: In iOS 13, Apple made a change that results in video URLs returned by UIImagePickerController becoming invalidated as soon as the info object from the delegate callback is released. This commit works around this issue by retaining these info objects by default and giving the application a way to release them once it is done processing the video. See also https://stackoverflow.com/questions/57798968/didfinishpickingmediawithinfo-returns-different-url-in-ios-13 Reviewed By: olegbl, mmmulani Differential Revision: D17845889 fbshipit-source-id: 12d0e496508dafa2581ef12730f7537ef98c60e2 --- Libraries/CameraRoll/RCTImagePickerManager.mm | 30 ++++++++++++++++++- .../FBReactNativeSpec-generated.mm | 14 +++++++++ .../FBReactNativeSpec/FBReactNativeSpec.h | 2 ++ Libraries/Image/ImagePickerIOS.js | 20 +++++++++++++ Libraries/Image/NativeImagePickerIOS.js | 2 ++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Libraries/CameraRoll/RCTImagePickerManager.mm b/Libraries/CameraRoll/RCTImagePickerManager.mm index 9f0b2321087170..14a743b6ed4d62 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.mm +++ b/Libraries/CameraRoll/RCTImagePickerManager.mm @@ -37,6 +37,7 @@ @implementation RCTImagePickerManager NSMutableArray *_pickers; NSMutableArray *_pickerCallbacks; NSMutableArray *_pickerCancelCallbacks; + NSMutableDictionary *> *_pendingVideoInfo; } RCT_EXPORT_MODULE(ImagePickerIOS); @@ -133,6 +134,24 @@ - (dispatch_queue_t)methodQueue cancelCallback:cancelCallback]; } +// In iOS 13, the URLs provided when selecting videos from the library are only valid while the +// info object provided by the delegate is retained. +// This method provides a way to clear out all retained pending info objects. +RCT_EXPORT_METHOD(clearAllPendingVideos) +{ + [_pendingVideoInfo removeAllObjects]; + _pendingVideoInfo = [NSMutableDictionary new]; +} + +// In iOS 13, the URLs provided when selecting videos from the library are only valid while the +// info object provided by the delegate is retained. +// This method provides a way to release the info object for a particular file url when the application +// is done with it, for example after the video has been uploaded or copied locally. +RCT_EXPORT_METHOD(removePendingVideo:(NSString *)url) +{ + [_pendingVideoInfo removeObjectForKey:url]; +} + - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { @@ -148,7 +167,15 @@ - (void)imagePickerController:(UIImagePickerController *)picker width = @(image.size.width); } if (imageURL) { - [self _dismissPicker:picker args:@[imageURL.absoluteString, RCTNullIfNil(height), RCTNullIfNil(width)]]; + NSString *imageURLString = imageURL.absoluteString; + // In iOS 13, video URLs are only valid while info dictionary is retained + if (@available(iOS 13.0, *)) { + if (isMovie) { + _pendingVideoInfo[imageURLString] = info; + } + } + + [self _dismissPicker:picker args:@[imageURLString, RCTNullIfNil(height), RCTNullIfNil(width)]]; return; } @@ -176,6 +203,7 @@ - (void)_presentPicker:(UIImagePickerController *)imagePicker _pickers = [NSMutableArray new]; _pickerCallbacks = [NSMutableArray new]; _pickerCancelCallbacks = [NSMutableArray new]; + _pendingVideoInfo = [NSMutableDictionary new]; } [_pickers addObject:imagePicker]; diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index 6a9618d5461184..3a0280683b5191 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -1238,6 +1238,14 @@ + (RCTManagedPointer *)JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:(id)js return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "openSelectDialog", @selector(openSelectDialog:successCallback:cancelCallback:), args, count); } + static facebook::jsi::Value __hostFunction_NativeImagePickerIOSSpecJSI_clearAllPendingVideos(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "clearAllPendingVideos", @selector(clearAllPendingVideos), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeImagePickerIOSSpecJSI_removePendingVideo(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "removePendingVideo", @selector(removePendingVideo:), args, count); + } + NativeImagePickerIOSSpecJSI::NativeImagePickerIOSSpecJSI(id instance, std::shared_ptr jsInvoker) : ObjCTurboModule("ImagePickerIOS", instance, jsInvoker) { @@ -1256,6 +1264,12 @@ + (RCTManagedPointer *)JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:(id)js setMethodArgConversionSelector(@"openSelectDialog", 0, @"JS_NativeImagePickerIOS_SpecOpenSelectDialogConfig:"); + methodMap_["clearAllPendingVideos"] = MethodMetadata {0, __hostFunction_NativeImagePickerIOSSpecJSI_clearAllPendingVideos}; + + + methodMap_["removePendingVideo"] = MethodMetadata {1, __hostFunction_NativeImagePickerIOSSpecJSI_removePendingVideo}; + + } diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 87f92a51b03fe2..c3fe3f35d86ab2 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -1218,6 +1218,8 @@ namespace JS { - (void)openSelectDialog:(JS::NativeImagePickerIOS::SpecOpenSelectDialogConfig &)config successCallback:(RCTResponseSenderBlock)successCallback cancelCallback:(RCTResponseSenderBlock)cancelCallback; +- (void)clearAllPendingVideos; +- (void)removePendingVideo:(NSString *)url; @end namespace facebook { diff --git a/Libraries/Image/ImagePickerIOS.js b/Libraries/Image/ImagePickerIOS.js index 587d89b6ed6fe8..a7f0049d548a4d 100644 --- a/Libraries/Image/ImagePickerIOS.js +++ b/Libraries/Image/ImagePickerIOS.js @@ -80,6 +80,26 @@ const ImagePickerIOS = { cancelCallback, ); }, + /** + * In iOS 13, the video URLs returned by the Image Picker are invalidated when + * the picker is dismissed, unless reference to it is held. This API allows + * the application to signal when it's finished with the video so that the + * reference can be cleaned up. + * It is safe to call this method for urlsthat aren't video URLs; + * it will be a no-op. + */ + removePendingVideo: function(url: string): void { + invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available'); + NativeImagePickerIOS.removePendingVideo(url); + }, + /** + * WARNING: In most cases, removePendingVideo should be used instead because + * clearAllPendingVideos could clear out pending videos made by other callers. + */ + clearAllPendingVideos: function(): void { + invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available'); + NativeImagePickerIOS.clearAllPendingVideos(); + }, }; module.exports = ImagePickerIOS; diff --git a/Libraries/Image/NativeImagePickerIOS.js b/Libraries/Image/NativeImagePickerIOS.js index 895f95f39f69d1..6101c7b0380748 100644 --- a/Libraries/Image/NativeImagePickerIOS.js +++ b/Libraries/Image/NativeImagePickerIOS.js @@ -33,6 +33,8 @@ export interface Spec extends TurboModule { successCallback: (imageURL: string, height: number, width: number) => void, cancelCallback: () => void, ) => void; + +clearAllPendingVideos: () => void; + +removePendingVideo: (url: string) => void; } export default (TurboModuleRegistry.get('ImagePickerIOS'): ?Spec);