1 import ABI49_0_0ExpoModulesCore 2 import UIKit 3 import MobileCoreServices 4 5 struct PickingContext { 6 let promise: Promise 7 let options: DocumentPickerOptions 8 let delegate: DocumentPickingDelegate 9 } 10 11 public class DocumentPickerModule: Module, PickingResultHandler { 12 private var pickingContext: PickingContext? 13 definitionnull14 public func definition() -> ModuleDefinition { 15 Name("ExpoDocumentPicker") 16 17 AsyncFunction("getDocumentAsync") { (options: DocumentPickerOptions, promise: Promise) in 18 if pickingContext != nil { 19 throw PickingInProgressException() 20 } 21 22 guard let currentVc = appContext?.utilities?.currentViewController() else { 23 throw MissingViewControllerException() 24 } 25 26 let documentPickerVC = createDocumentPicker(with: options) 27 let pickerDelegate = DocumentPickingDelegate(resultHandler: self) 28 29 pickingContext = PickingContext(promise: promise, options: options, delegate: pickerDelegate) 30 31 documentPickerVC.delegate = pickerDelegate 32 documentPickerVC.presentationController?.delegate = pickerDelegate 33 documentPickerVC.allowsMultipleSelection = options.multiple 34 35 if UIDevice.current.userInterfaceIdiom == .pad { 36 let viewFrame = currentVc.view.frame 37 documentPickerVC.popoverPresentationController?.sourceRect = CGRect( 38 x: viewFrame.midX, 39 y: viewFrame.maxY, 40 width: 0, 41 height: 0 42 ) 43 documentPickerVC.popoverPresentationController?.sourceView = currentVc.view 44 documentPickerVC.modalPresentationStyle = .pageSheet 45 } 46 currentVc.present(documentPickerVC, animated: true) 47 }.runOnQueue(.main) 48 } 49 didPickDocumentsAtnull50 func didPickDocumentsAt(urls: [URL]) { 51 guard let options = self.pickingContext?.options, 52 let promise = self.pickingContext?.promise else { 53 log.error("Picking context has been lost.") 54 return 55 } 56 pickingContext = nil 57 58 do { 59 if options.multiple { 60 let assets = try urls.map { 61 try readDocumentDetails( 62 documentUrl: $0, 63 copy: options.copyToCacheDirectory 64 ) 65 } 66 promise.resolve(DocumentPickerResponse(assets: assets)) 67 } else { 68 let asset = try readDocumentDetails( 69 documentUrl: urls[0], 70 copy: options.copyToCacheDirectory 71 ) 72 promise.resolve(DocumentPickerResponse(assets: [asset])) 73 } 74 } catch { 75 promise.reject(error) 76 } 77 } 78 didCancelPickingnull79 func didCancelPicking() { 80 guard let context = pickingContext else { 81 log.error("Picking context lost") 82 return 83 } 84 85 pickingContext = nil 86 context.promise.resolve(DocumentPickerResponse(canceled: true)) 87 } 88 getFileSizenull89 private func getFileSize(path: URL) -> Int? { 90 guard let resource = try? path.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) else { 91 return 0 92 } 93 94 if let isDirectory = resource.isDirectory { 95 if !isDirectory { 96 return resource.fileSize 97 } 98 } 99 100 guard let contents = try? FileManager.default.contentsOfDirectory(atPath: path.absoluteString) else { 101 return 0 102 } 103 104 let folderSize = contents.reduce(0) { currentSize, file in 105 let fileSize = getFileSize(path: path.appendingPathComponent(file)) ?? 0 106 return currentSize + fileSize 107 } 108 109 return folderSize 110 } 111 readDocumentDetailsnull112 private func readDocumentDetails(documentUrl: URL, copy: Bool) throws -> DocumentInfo { 113 var newUrl = documentUrl 114 115 guard let fileSystem = self.appContext?.fileSystem else { 116 throw Exceptions.FileSystemModuleNotFound() 117 } 118 119 guard let fileSize = try? getFileSize(path: documentUrl) else { 120 throw InvalidFileException() 121 } 122 123 if copy { 124 let cacheDirURL = URL(fileURLWithPath: fileSystem.cachesDirectory) 125 let directory = cacheDirURL.appendingPathComponent("DocumentPicker", isDirectory: true).path 126 let fileExtension = "." + documentUrl.pathExtension 127 let path = fileSystem.generatePath(inDirectory: directory, withExtension: fileExtension) 128 newUrl = URL(fileURLWithPath: path) 129 130 try FileManager.default.copyItem(at: documentUrl, to: newUrl) 131 } 132 133 let mimeType = self.getMimeType(from: documentUrl.pathExtension) 134 135 return DocumentInfo( 136 uri: newUrl.absoluteString, 137 name: documentUrl.lastPathComponent, 138 size: fileSize, 139 mimeType: mimeType 140 ) 141 } 142 getMimeTypenull143 private func getMimeType(from pathExtension: String) -> String? { 144 if #available(iOS 14, *) { 145 return UTType(filenameExtension: pathExtension)?.preferredMIMEType 146 } else { 147 if let uti = UTTypeCreatePreferredIdentifierForTag( 148 kUTTagClassFilenameExtension, 149 pathExtension as NSString, nil 150 )?.takeRetainedValue() { 151 if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { 152 return mimetype as String 153 } 154 } 155 return nil 156 } 157 } 158 159 @available(iOS 14.0, *) toUTTypenull160 private func toUTType(mimeType: String) -> UTType? { 161 switch mimeType { 162 case "*/*": 163 return UTType.item 164 case "image/*": 165 return UTType.image 166 case "video/*": 167 return UTType.video 168 case "audio/*": 169 return UTType.audio 170 case "text/*": 171 return UTType.text 172 default: 173 return UTType(mimeType: mimeType) 174 } 175 } 176 toUTInull177 private func toUTI(mimeType: String) -> String { 178 var uti: CFString 179 180 switch mimeType { 181 case "*/*": 182 uti = kUTTypeItem 183 case "image/*": 184 uti = kUTTypeImage 185 case "video/*": 186 uti = kUTTypeVideo 187 case "audio/*": 188 uti = kUTTypeAudio 189 case "text/*": 190 uti = kUTTypeText 191 default: 192 if let ref = UTTypeCreatePreferredIdentifierForTag( 193 kUTTagClassMIMEType, 194 mimeType as CFString, 195 nil 196 )?.takeRetainedValue() { 197 uti = ref 198 } else { 199 uti = kUTTypeItem 200 } 201 } 202 return uti as String 203 } 204 createDocumentPickernull205 private func createDocumentPicker(with options: DocumentPickerOptions) -> UIDocumentPickerViewController { 206 if #available(iOS 14.0, *) { 207 let utTypes = options.type.compactMap { toUTType(mimeType: $0) } 208 return UIDocumentPickerViewController( 209 forOpeningContentTypes: utTypes, 210 asCopy: true 211 ) 212 } else { 213 let utiTypes = options.type.map { toUTI(mimeType: $0) } 214 return UIDocumentPickerViewController( 215 documentTypes: utiTypes, 216 in: .import 217 ) 218 } 219 } 220 } 221