// Copyright 2022-present 650 Industries. All rights reserved. import UIKit import PhotosUI import ABI49_0_0ExpoModulesCore typealias MediaInfo = [UIImagePickerController.InfoKey: Any] /** Helper struct storing single picking operation context variables that have their own non-sharable state. */ struct PickingContext { let promise: Promise let options: ImagePickerOptions let imagePickerHandler: ImagePickerHandler } enum OperationType { case ask case get } public class ImagePickerModule: Module, OnMediaPickingResultHandler { public func definition() -> ModuleDefinition { // TODO: (@bbarthec) change to "ExpoImagePicker" and propagate to other platforms Name("ExponentImagePicker") OnCreate { self.appContext?.permissions?.register([ CameraPermissionRequester(), MediaLibraryPermissionRequester(), MediaLibraryWriteOnlyPermissionRequester() ]) } AsyncFunction("getCameraPermissionsAsync", { (promise: Promise) in self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .get, promise: promise) }) AsyncFunction("getMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .get, promise: promise) }) AsyncFunction("requestCameraPermissionsAsync", { (promise: Promise) in self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .ask, promise: promise) }) AsyncFunction("requestMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .ask, promise: promise) }) AsyncFunction("launchCameraAsync", { (options: ImagePickerOptions, promise: Promise) -> Void in guard let permissions = self.appContext?.permissions else { return promise.reject(PermissionsModuleNotFoundException()) } guard permissions.hasGrantedPermission(usingRequesterClass: CameraPermissionRequester.self) else { return promise.reject(MissingCameraPermissionException()) } self.launchImagePicker(sourceType: .camera, options: options, promise: promise) }) .runOnQueue(DispatchQueue.main) AsyncFunction("launchImageLibraryAsync", { (options: ImagePickerOptions, promise: Promise) in self.launchImagePicker(sourceType: .photoLibrary, options: options, promise: promise) }) .runOnQueue(DispatchQueue.main) } private var currentPickingContext: PickingContext? private func handlePermissionRequest(requesterClass: AnyClass, operationType: OperationType, promise: Promise) { guard let permissions = self.appContext?.permissions else { return promise.reject(PermissionsModuleNotFoundException()) } switch operationType { case .get: permissions.getPermissionUsingRequesterClass(requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter) case .ask: permissions.askForPermission(usingRequesterClass: requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter) } } private func getMediaLibraryPermissionRequester(_ writeOnly: Bool) -> AnyClass { return writeOnly ? MediaLibraryWriteOnlyPermissionRequester.self : MediaLibraryPermissionRequester.self } private func launchImagePicker(sourceType: UIImagePickerController.SourceType, options: ImagePickerOptions, promise: Promise) { let imagePickerDelegate = ImagePickerHandler(onMediaPickingResultHandler: self, hideStatusBarWhenPresented: options.allowsEditing && !options.allowsMultipleSelection) let pickingContext = PickingContext(promise: promise, options: options, imagePickerHandler: imagePickerDelegate) if #available(iOS 14, *), !options.allowsEditing && sourceType != .camera { self.launchMultiSelectPicker(pickingContext: pickingContext) } else { self.launchLegacyImagePicker(sourceType: sourceType, pickingContext: pickingContext) } } private func launchLegacyImagePicker(sourceType: UIImagePickerController.SourceType, pickingContext: PickingContext) { let options = pickingContext.options let picker = UIImagePickerController() if sourceType == .camera { #if targetEnvironment(simulator) return pickingContext.promise.reject(CameraUnavailableOnSimulatorException()) #else picker.sourceType = .camera picker.cameraDevice = options.cameraType == .front ? .front : .rear #endif } if sourceType == .photoLibrary { picker.sourceType = .photoLibrary } picker.mediaTypes = options.mediaTypes.toArray() picker.videoExportPreset = options.videoExportPreset.toAVAssetExportPreset() picker.videoQuality = options.videoQuality.toQualityType() picker.videoMaximumDuration = options.videoMaxDuration if options.allowsEditing { picker.allowsEditing = options.allowsEditing if options.videoMaxDuration > 600 { return pickingContext.promise.reject(MaxDurationWhileEditingExceededException()) } if options.videoMaxDuration == 0 { picker.videoMaximumDuration = 600.0 } } presentPickerUI(picker, pickingContext: pickingContext) } @available(iOS 14, *) private func launchMultiSelectPicker(pickingContext: PickingContext) { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) let options = pickingContext.options // selection limit = 1 --> single selection, reflects the old picker behavior configuration.selectionLimit = options.allowsMultipleSelection ? options.selectionLimit : SINGLE_SELECTION configuration.filter = options.mediaTypes.toPickerFilter() if #available(iOS 14, *) { configuration.preferredAssetRepresentationMode = options.preferredAssetRepresentationMode.toAssetRepresentationMode() } if #available(iOS 15, *) { configuration.selection = options.orderedSelection ? .ordered : .default } let picker = PHPickerViewController(configuration: configuration) presentPickerUI(picker, pickingContext: pickingContext) } private func presentPickerUI(_ picker: PickerUIController, pickingContext context: PickingContext) { guard let currentViewController = self.appContext?.utilities?.currentViewController() else { return context.promise.reject(MissingCurrentViewControllerException()) } picker.modalPresentationStyle = context.options.presentationStyle.toPresentationStyle() picker.setResultHandler(context.imagePickerHandler) // Store picking context as we're navigating to the different view controller (starting asynchronous flow) self.currentPickingContext = context currentViewController.present(picker, animated: true, completion: nil) } // MARK: - OnMediaPickingResultHandler func didCancelPicking() { self.currentPickingContext?.promise.resolve(ImagePickerResponse(assets: nil, canceled: true)) self.currentPickingContext = nil } @available(iOS 14, *) func didPickMultipleMedia(selection: [PHPickerResult]) { guard let options = self.currentPickingContext?.options, let promise = self.currentPickingContext?.promise else { log.error("Picking operation context has been lost.") return } guard let fileSystem = self.appContext?.fileSystem else { return promise.reject(FileSystemModuleNotFoundException()) } let mediaHandler = MediaHandler(fileSystem: fileSystem, options: options) // Clean up the currently stored picking context self.currentPickingContext = nil mediaHandler.handleMultipleMedia(selection) { result -> Void in switch result { case .failure(let error): return promise.reject(error) case .success(let response): return promise.resolve(response) } } } func didPickMedia(mediaInfo: MediaInfo) { guard let options = self.currentPickingContext?.options, let promise = self.currentPickingContext?.promise else { log.error("Picking operation context has been lost.") return } guard let fileSystem = self.appContext?.fileSystem else { return promise.reject(FileSystemModuleNotFoundException()) } // Clean up the currently stored picking context self.currentPickingContext = nil let mediaHandler = MediaHandler(fileSystem: fileSystem, options: options) mediaHandler.handleMedia(mediaInfo) { result -> Void in switch result { case .failure(let error): return promise.reject(error) case .success(let response): return promise.resolve(response) } } } }