1 // Copyright 2022-present 650 Industries. All rights reserved. 2 3 import UIKit 4 import PhotosUI 5 import ABI48_0_0ExpoModulesCore 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 106 if sourceType == .camera { 107 #if targetEnvironment(simulator) 108 return pickingContext.promise.reject(CameraUnavailableOnSimulatorException()) 109 #else 110 picker.sourceType = .camera 111 picker.cameraDevice = options.cameraType == .front ? .front : .rear 112 #endif 113 } 114 115 if sourceType == .photoLibrary { 116 picker.sourceType = .photoLibrary 117 } 118 119 picker.mediaTypes = options.mediaTypes.toArray() 120 picker.videoExportPreset = options.videoExportPreset.toAVAssetExportPreset() 121 picker.videoQuality = options.videoQuality.toQualityType() 122 picker.videoMaximumDuration = options.videoMaxDuration 123 124 if options.allowsEditing { 125 picker.allowsEditing = options.allowsEditing 126 if options.videoMaxDuration > 600 { 127 return pickingContext.promise.reject(MaxDurationWhileEditingExceededException()) 128 } 129 if options.videoMaxDuration == 0 { 130 picker.videoMaximumDuration = 600.0 131 } 132 } 133 134 presentPickerUI(picker, pickingContext: pickingContext) 135 } 136 137 @available(iOS 14, *) launchMultiSelectPickernull138 private func launchMultiSelectPicker(pickingContext: PickingContext) { 139 var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) 140 let options = pickingContext.options 141 142 // selection limit = 1 --> single selection, reflects the old picker behavior 143 configuration.selectionLimit = options.allowsMultipleSelection ? options.selectionLimit : SINGLE_SELECTION 144 configuration.filter = options.mediaTypes.toPickerFilter() 145 if #available(iOS 15, *) { 146 configuration.selection = options.orderedSelection ? .ordered : .default 147 } 148 149 let picker = PHPickerViewController(configuration: configuration) 150 151 presentPickerUI(picker, pickingContext: pickingContext) 152 } 153 presentPickerUInull154 private func presentPickerUI(_ picker: PickerUIController, pickingContext context: PickingContext) { 155 guard let currentViewController = self.appContext?.utilities?.currentViewController() else { 156 return context.promise.reject(MissingCurrentViewControllerException()) 157 } 158 159 picker.modalPresentationStyle = context.options.presentationStyle.toPresentationStyle() 160 picker.setResultHandler(context.imagePickerHandler) 161 162 // Store picking context as we're navigating to the different view controller (starting asynchronous flow) 163 self.currentPickingContext = context 164 currentViewController.present(picker, animated: true, completion: nil) 165 } 166 167 // MARK: - OnMediaPickingResultHandler 168 didCancelPickingnull169 func didCancelPicking() { 170 self.currentPickingContext?.promise.resolve(ImagePickerResponse(assets: nil, canceled: true)) 171 self.currentPickingContext = nil 172 } 173 174 @available(iOS 14, *) didPickMultipleMedianull175 func didPickMultipleMedia(selection: [PHPickerResult]) { 176 guard let options = self.currentPickingContext?.options, 177 let promise = self.currentPickingContext?.promise else { 178 log.error("Picking operation context has been lost.") 179 return 180 } 181 guard let fileSystem = self.appContext?.fileSystem else { 182 return promise.reject(FileSystemModuleNotFoundException()) 183 } 184 185 let mediaHandler = MediaHandler(fileSystem: fileSystem, 186 options: options) 187 188 // Clean up the currently stored picking context 189 self.currentPickingContext = nil 190 191 mediaHandler.handleMultipleMedia(selection) { result -> Void in 192 switch result { 193 case .failure(let error): return promise.reject(error) 194 case .success(let response): return promise.resolve(response) 195 } 196 } 197 } 198 didPickMedianull199 func didPickMedia(mediaInfo: MediaInfo) { 200 guard let options = self.currentPickingContext?.options, 201 let promise = self.currentPickingContext?.promise else { 202 log.error("Picking operation context has been lost.") 203 return 204 } 205 guard let fileSystem = self.appContext?.fileSystem else { 206 return promise.reject(FileSystemModuleNotFoundException()) 207 } 208 209 // Clean up the currently stored picking context 210 self.currentPickingContext = nil 211 212 let mediaHandler = MediaHandler(fileSystem: fileSystem, 213 options: options) 214 mediaHandler.handleMedia(mediaInfo) { result -> Void in 215 switch result { 216 case .failure(let error): return promise.reject(error) 217 case .success(let response): return promise.resolve(response) 218 } 219 } 220 } 221 } 222