1 // Copyright 2022-present 650 Industries. All rights reserved. 2 3 import UIKit 4 import PhotosUI 5 import ExpoModulesCore 6 7 typealias MediaInfo = [UIImagePickerController.InfoKey: Any] 8 9 /** 10 Helper struct storing single picking operation context variables that have their own non-sharable state. 11 */ 12 struct PickingContext { 13 let promise: Promise 14 let options: ImagePickerOptions 15 let imagePickerHandler: ImagePickerHandler 16 } 17 18 enum OperationType { 19 case ask 20 case get 21 } 22 23 public class ImagePickerModule: Module, OnMediaPickingResultHandler { definitionnull24 public func definition() -> ModuleDefinition { 25 // TODO: (@bbarthec) change to "ExpoImagePicker" and propagate to other platforms 26 Name("ExponentImagePicker") 27 28 OnCreate { 29 self.appContext?.permissions?.register([ 30 CameraPermissionRequester(), 31 MediaLibraryPermissionRequester(), 32 MediaLibraryWriteOnlyPermissionRequester() 33 ]) 34 } 35 36 AsyncFunction("getCameraPermissionsAsync", { (promise: Promise) in 37 self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .get, promise: promise) 38 }) 39 40 AsyncFunction("getMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in 41 self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .get, promise: promise) 42 }) 43 44 AsyncFunction("requestCameraPermissionsAsync", { (promise: Promise) in 45 self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .ask, promise: promise) 46 }) 47 48 AsyncFunction("requestMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in 49 self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .ask, promise: promise) 50 }) 51 52 AsyncFunction("launchCameraAsync", { (options: ImagePickerOptions, promise: Promise) -> Void in 53 guard let permissions = self.appContext?.permissions else { 54 return promise.reject(PermissionsModuleNotFoundException()) 55 } 56 57 guard permissions.hasGrantedPermission(usingRequesterClass: CameraPermissionRequester.self) else { 58 return promise.reject(MissingCameraPermissionException()) 59 } 60 61 self.launchImagePicker(sourceType: .camera, options: options, promise: promise) 62 }) 63 .runOnQueue(DispatchQueue.main) 64 65 AsyncFunction("launchImageLibraryAsync", { (options: ImagePickerOptions, promise: Promise) in 66 self.launchImagePicker(sourceType: .photoLibrary, options: options, promise: promise) 67 }) 68 .runOnQueue(DispatchQueue.main) 69 } 70 71 private var currentPickingContext: PickingContext? 72 handlePermissionRequestnull73 private func handlePermissionRequest(requesterClass: AnyClass, operationType: OperationType, promise: Promise) { 74 guard let permissions = self.appContext?.permissions else { 75 return promise.reject(PermissionsModuleNotFoundException()) 76 } 77 switch operationType { 78 case .get: permissions.getPermissionUsingRequesterClass(requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter) 79 case .ask: permissions.askForPermission(usingRequesterClass: requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter) 80 } 81 } 82 getMediaLibraryPermissionRequesternull83 private func getMediaLibraryPermissionRequester(_ writeOnly: Bool) -> AnyClass { 84 return writeOnly ? MediaLibraryWriteOnlyPermissionRequester.self : MediaLibraryPermissionRequester.self 85 } 86 launchImagePickernull87 private func launchImagePicker(sourceType: UIImagePickerController.SourceType, options: ImagePickerOptions, promise: Promise) { 88 let imagePickerDelegate = ImagePickerHandler(onMediaPickingResultHandler: self, hideStatusBarWhenPresented: options.allowsEditing && !options.allowsMultipleSelection) 89 90 let pickingContext = PickingContext(promise: promise, 91 options: options, 92 imagePickerHandler: imagePickerDelegate) 93 94 if #available(iOS 14, *), !options.allowsEditing && sourceType != .camera { 95 self.launchMultiSelectPicker(pickingContext: pickingContext) 96 } else { 97 self.launchLegacyImagePicker(sourceType: sourceType, pickingContext: pickingContext) 98 } 99 } 100 launchLegacyImagePickernull101 private func launchLegacyImagePicker(sourceType: UIImagePickerController.SourceType, pickingContext: PickingContext) { 102 let options = pickingContext.options 103 104 let picker = UIImagePickerController() 105 picker.fixCannotMoveEditingBox() 106 107 if sourceType == .camera { 108 #if targetEnvironment(simulator) 109 return pickingContext.promise.reject(CameraUnavailableOnSimulatorException()) 110 #else 111 picker.sourceType = .camera 112 picker.cameraDevice = options.cameraType == .front ? .front : .rear 113 #endif 114 } 115 116 if sourceType == .photoLibrary { 117 picker.sourceType = .photoLibrary 118 } 119 120 picker.mediaTypes = options.mediaTypes.toArray() 121 picker.videoExportPreset = options.videoExportPreset.toAVAssetExportPreset() 122 picker.videoQuality = options.videoQuality.toQualityType() 123 picker.videoMaximumDuration = options.videoMaxDuration 124 125 if options.allowsEditing { 126 picker.allowsEditing = options.allowsEditing 127 if options.videoMaxDuration > 600 { 128 return pickingContext.promise.reject(MaxDurationWhileEditingExceededException()) 129 } 130 if options.videoMaxDuration == 0 { 131 picker.videoMaximumDuration = 600.0 132 } 133 } 134 135 presentPickerUI(picker, pickingContext: pickingContext) 136 } 137 138 @available(iOS 14, *) launchMultiSelectPickernull139 private func launchMultiSelectPicker(pickingContext: PickingContext) { 140 var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) 141 let options = pickingContext.options 142 143 // selection limit = 1 --> single selection, reflects the old picker behavior 144 configuration.selectionLimit = options.allowsMultipleSelection ? options.selectionLimit : SINGLE_SELECTION 145 configuration.filter = options.mediaTypes.toPickerFilter() 146 if #available(iOS 14, *) { 147 configuration.preferredAssetRepresentationMode = options.preferredAssetRepresentationMode.toAssetRepresentationMode() 148 } 149 if #available(iOS 15, *) { 150 configuration.selection = options.orderedSelection ? .ordered : .default 151 } 152 153 let picker = PHPickerViewController(configuration: configuration) 154 155 presentPickerUI(picker, pickingContext: pickingContext) 156 } 157 presentPickerUInull158 private func presentPickerUI(_ picker: PickerUIController, pickingContext context: PickingContext) { 159 guard let currentViewController = self.appContext?.utilities?.currentViewController() else { 160 return context.promise.reject(MissingCurrentViewControllerException()) 161 } 162 163 picker.modalPresentationStyle = context.options.presentationStyle.toPresentationStyle() 164 picker.setResultHandler(context.imagePickerHandler) 165 166 // Store picking context as we're navigating to the different view controller (starting asynchronous flow) 167 self.currentPickingContext = context 168 currentViewController.present(picker, animated: true, completion: nil) 169 } 170 171 // MARK: - OnMediaPickingResultHandler 172 didCancelPickingnull173 func didCancelPicking() { 174 self.currentPickingContext?.promise.resolve(ImagePickerResponse(assets: nil, canceled: true)) 175 self.currentPickingContext = nil 176 } 177 178 @available(iOS 14, *) didPickMultipleMedianull179 func didPickMultipleMedia(selection: [PHPickerResult]) { 180 guard let options = self.currentPickingContext?.options, 181 let promise = self.currentPickingContext?.promise else { 182 log.error("Picking operation context has been lost.") 183 return 184 } 185 guard let fileSystem = self.appContext?.fileSystem else { 186 return promise.reject(FileSystemModuleNotFoundException()) 187 } 188 189 let mediaHandler = MediaHandler(fileSystem: fileSystem, 190 options: options) 191 192 // Clean up the currently stored picking context 193 self.currentPickingContext = nil 194 195 mediaHandler.handleMultipleMedia(selection) { result -> Void in 196 switch result { 197 case .failure(let error): return promise.reject(error) 198 case .success(let response): return promise.resolve(response) 199 } 200 } 201 } 202 didPickMedianull203 func didPickMedia(mediaInfo: MediaInfo) { 204 guard let options = self.currentPickingContext?.options, 205 let promise = self.currentPickingContext?.promise else { 206 log.error("Picking operation context has been lost.") 207 return 208 } 209 guard let fileSystem = self.appContext?.fileSystem else { 210 return promise.reject(FileSystemModuleNotFoundException()) 211 } 212 213 // Clean up the currently stored picking context 214 self.currentPickingContext = nil 215 216 let mediaHandler = MediaHandler(fileSystem: fileSystem, 217 options: options) 218 mediaHandler.handleMedia(mediaInfo) { result -> Void in 219 switch result { 220 case .failure(let error): return promise.reject(error) 221 case .success(let response): return promise.resolve(response) 222 } 223 } 224 } 225 } 226