diff --git a/ios/ExpoShareIntentModule.swift b/ios/ExpoShareIntentModule.swift index 23f28d2..277e046 100644 --- a/ios/ExpoShareIntentModule.swift +++ b/ios/ExpoShareIntentModule.swift @@ -1,7 +1,7 @@ /*! * Native module created for Expo Share Intent (https://github.com/achorein/expo-share-intent) * author: achorein (https://github.com/achorein) - * inspired by : + * inspired by : * - https://github.com/ajith-ab/react-native-receive-sharing-intent/blob/master/ios/ReceiveSharingIntent.swift */ import ExpoModulesCore @@ -9,206 +9,232 @@ import Foundation import Photos public class ExpoShareIntentModule: Module { - // Each module class must implement the definition function. The definition consists of components - // that describes the module's functionality and behavior. - // See https://docs.expo.dev/modules/module-api for more details about available components. - public func definition() -> ModuleDefinition { - // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. - // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. - // The module will be accessible from `requireNativeModule('ExpoShareIntentModule')` in JavaScript. - Name("ExpoShareIntentModule") - - Events("onChange", "onStateChange", "onError") - - // Defines a JavaScript function that always returns a Promise and whose native code - // is by default dispatched on the different thread than the JavaScript runtime runs on. - AsyncFunction("getShareIntent") { (url: String) in - let fileUrl = URL(string: url) - let json = handleUrl(url: fileUrl); - if (json != "error" && json != "empty") { - self.sendEvent("onChange", [ - "value": json - ]) + // Each module class must implement the definition function. The definition consists of components + // that describes the module's functionality and behavior. + // See https://docs.expo.dev/modules/module-api for more details about available components. + public func definition() -> ModuleDefinition { + // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. + // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. + // The module will be accessible from `requireNativeModule('ExpoShareIntentModule')` in JavaScript. + Name("ExpoShareIntentModule") + + Events("onChange", "onStateChange", "onError") + + // Defines a JavaScript function that always returns a Promise and whose native code + // is by default dispatched on the different thread than the JavaScript runtime runs on. + AsyncFunction("getShareIntent") { (url: String) in + let fileUrl = URL(string: url) + let json = handleUrl(url: fileUrl) + if json != "error" && json != "empty" { + self.sendEvent( + "onChange", + [ + "value": json + ]) + } } - } - Function("clearShareIntent") { (sharedKey: String) in - let userDefaults = UserDefaults(suiteName: "group.\(Bundle.main.bundleIdentifier!)") - userDefaults?.set(nil, forKey: sharedKey) - userDefaults?.synchronize() - } + Function("clearShareIntent") { (sharedKey: String) in + let userDefaults = UserDefaults(suiteName: "group.\(Bundle.main.bundleIdentifier!)") + userDefaults?.set(nil, forKey: sharedKey) + userDefaults?.synchronize() + } - Function("hasShareIntent") { (key: String) in - // for Android only - return false + Function("hasShareIntent") { (key: String) in + // for Android only + return false + } } - } - - private var initialMedia: [SharedMediaFile]? = nil - private var latestMedia: [SharedMediaFile]? = nil - - private var initialText: String? = nil - private var latestText: String? = nil - - private func handleUrl(url: URL?) -> String? { - let appDomain = Bundle.main.bundleIdentifier! - if let url = url { - let userDefaults = UserDefaults(suiteName: "group.\(appDomain)") - if url.fragment == "media" { - if let key = url.host?.components(separatedBy: "=").last { - if let json = userDefaults?.object(forKey: key) as? Data { - let sharedArray = decode(data: json) - let sharedMediaFiles: [SharedMediaFile] = sharedArray.compactMap { - if let path = getAbsolutePath(for: $0.path) { - if ($0.type == .video && $0.thumbnail != nil) { - let thumbnail = getAbsolutePath(for: $0.thumbnail!) - return SharedMediaFile.init(path: path, thumbnail: thumbnail, fileName: $0.fileName, fileSize: $0.fileSize, width: $0.width, height: $0.height, duration: $0.duration, mimeType: $0.mimeType, type: $0.type) - } else if ($0.type == .video && $0.thumbnail == nil) { - return SharedMediaFile.init(path: path, thumbnail: nil, fileName: $0.fileName, fileSize: $0.fileSize, width: $0.width, height: $0.height, duration: $0.duration, mimeType: $0.mimeType, type: $0.type) + + private var initialMedia: [SharedMediaFile]? = nil + private var latestMedia: [SharedMediaFile]? = nil + + private var initialText: String? = nil + private var latestText: String? = nil + + private func handleUrl(url: URL?) -> String? { + let appDomain = Bundle.main.bundleIdentifier! + if let url = url { + let userDefaults = UserDefaults(suiteName: "group.\(appDomain)") + if url.fragment == "media" { + if let key = url.host?.components(separatedBy: "=").last { + if let json = userDefaults?.object(forKey: key) as? Data { + let sharedArray = decode(data: json) + let sharedMediaFiles: [SharedMediaFile] = sharedArray.compactMap { + if let path = getAbsolutePath(for: $0.path) { + if $0.type == .video && $0.thumbnail != nil { + let thumbnail = getAbsolutePath(for: $0.thumbnail!) + return SharedMediaFile.init( + path: path, thumbnail: thumbnail, fileName: $0.fileName, + fileSize: $0.fileSize, width: $0.width, height: $0.height, + duration: $0.duration, mimeType: $0.mimeType, type: $0.type) + } else if $0.type == .video && $0.thumbnail == nil { + return SharedMediaFile.init( + path: path, thumbnail: nil, fileName: $0.fileName, + fileSize: $0.fileSize, width: $0.width, height: $0.height, + duration: $0.duration, mimeType: $0.mimeType, type: $0.type) + } + return SharedMediaFile.init( + path: path, thumbnail: nil, fileName: $0.fileName, + fileSize: $0.fileSize, width: $0.width, height: $0.height, + duration: $0.duration, mimeType: $0.mimeType, type: $0.type) } - return SharedMediaFile.init(path: path, thumbnail: nil, fileName: $0.fileName, fileSize: $0.fileSize, width: $0.width, height: $0.height, duration: $0.duration, mimeType: $0.mimeType, type: $0.type) + return nil } - return nil + guard let json = toJson(data: sharedMediaFiles) else { return "[]" } + return "{ \"files\": \(json), \"type\": \"\(url.fragment!)\" }" + } else { + return "empty" } - guard let json = toJson(data: sharedMediaFiles) else { return "[]"}; - return "{ \"files\": \(json), \"type\": \"\(url.fragment!)\" }"; - } else { - return "empty" } - } - } else if url.fragment == "file" { - if let key = url.host?.components(separatedBy: "=").last { - if let json = userDefaults?.object(forKey: key) as? Data { - let sharedArray = decode(data: json) - let sharedMediaFiles: [SharedMediaFile] = sharedArray.compactMap{ - if let path = getAbsolutePath(for: $0.path) { - return SharedMediaFile.init(path: path, thumbnail: nil, fileName: $0.fileName, fileSize: $0.fileSize, width: nil, height: nil, duration: nil, mimeType: $0.mimeType, type: $0.type) + } else if url.fragment == "file" { + if let key = url.host?.components(separatedBy: "=").last { + if let json = userDefaults?.object(forKey: key) as? Data { + let sharedArray = decode(data: json) + let sharedMediaFiles: [SharedMediaFile] = sharedArray.compactMap { + if let path = getAbsolutePath(for: $0.path) { + return SharedMediaFile.init( + path: path, thumbnail: nil, fileName: $0.fileName, + fileSize: $0.fileSize, width: nil, height: nil, duration: nil, + mimeType: $0.mimeType, type: $0.type) + } + return nil } - return nil + guard let json = toJson(data: sharedMediaFiles) else { return "[]" } + return "{ \"files\": \(json), \"type\": \"\(url.fragment!)\" }" + } else { + return "empty" } - guard let json = toJson(data: sharedMediaFiles) else { return "[]"}; - return "{ \"files\": \(json), \"type\": \"\(url.fragment!)\" }"; - } else { - return "empty" } - } - } else if url.fragment == "text" || url.fragment == "weburl" { - if let key = url.host?.components(separatedBy: "=").last { - if let sharedArray = userDefaults?.object(forKey: key) as? [String] { - latestText = sharedArray.joined(separator: ",") - let optionalString = latestText; - if let unwrapped = optionalString { - return try? ShareIntentText(text: unwrapped, type: url.fragment!).toJSON() + } else if url.fragment == "text" || url.fragment == "weburl" { + if let key = url.host?.components(separatedBy: "=").last { + if let sharedArray = userDefaults?.object(forKey: key) as? [String] { + latestText = sharedArray.joined(separator: ",") + let optionalString = latestText + if let unwrapped = optionalString { + return try? ShareIntentText(text: unwrapped, type: url.fragment!) + .toJSON() + } + return latestText! + } else { + return "empty" } - return latestText!; + } + } else { + latestText = url.absoluteString + let optionalString = latestText + // now unwrap it + if let unwrapwebUrl = optionalString { + return try? ShareIntentText(text: unwrapwebUrl, type: url.fragment!).toJSON() } else { return "empty" } } - } else { - latestText = url.absoluteString - let optionalString = latestText; - // now unwrap it - if let unwrapwebUrl = optionalString { - return try? ShareIntentText(text: unwrapwebUrl, type: url.fragment!).toJSON() - } else { - return "empty" - } + self.sendEvent( + "onError", + [ + "value": "file type is Invalid \(url.fragment!)" + ]) + return "error" } - self.sendEvent("onError", [ - "value": "file type is Invalid \(url.fragment!)" - ]); + self.sendEvent( + "onError", + [ + "value": + "invalid group name. Please check your share extention bundle name is same as `group.\(appDomain)`" + ]) return "error" } - self.sendEvent("onError", [ - "value": "invalid group name. Please check your share extention bundle name is same as `group.\(appDomain)`" - ]) - return "error" - } - - private func getAbsolutePath(for identifier: String) -> String? { - if (identifier.starts(with: "file://") || identifier.starts(with: "/var/mobile/Media") || identifier.starts(with: "/private/var/mobile")) { - return identifier; + + private func getAbsolutePath(for identifier: String) -> String? { + if identifier.starts(with: "file://") || identifier.starts(with: "/var/mobile/Media") + || identifier.starts(with: "/private/var/mobile") + { + return identifier + } + let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: .none) + .firstObject + if phAsset == nil { + return nil + } + return getImageURL(for: phAsset!) + } + + private func getImageURL(for asset: PHAsset) -> String? { + var url: String? = nil + let semaphore = DispatchSemaphore(value: 0) + let options2 = PHContentEditingInputRequestOptions() + options2.isNetworkAccessAllowed = true + asset.requestContentEditingInput(with: options2) { (input, info) in + url = input?.fullSizeImageURL?.path + semaphore.signal() + } + semaphore.wait() + return url + } + + private func decode(data: Data) -> [SharedMediaFile] { + let encodedData = try? JSONDecoder().decode([SharedMediaFile].self, from: data) + return encodedData! + } + + private func toJson(data: [SharedMediaFile]?) -> String? { + if data == nil { + return nil + } + let encodedData = try? JSONEncoder().encode(data) + let json = String(data: encodedData!, encoding: .utf8)! + return json } - let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: .none).firstObject - if(phAsset == nil) { - return nil + + struct ShareIntentText: Codable { + let text: String + let type: String // text / weburl } - return getImageURL(for: phAsset!) - } - - private func getImageURL(for asset: PHAsset)-> String? { - var url: String? = nil - let semaphore = DispatchSemaphore(value: 0) - let options2 = PHContentEditingInputRequestOptions() - options2.isNetworkAccessAllowed = true - asset.requestContentEditingInput(with: options2){(input, info) in - url = input?.fullSizeImageURL?.path - semaphore.signal() + + class SharedMediaFile: Codable { + var path: String // can be image, video or url path + var thumbnail: String? // video thumbnail + var fileName: String // uuid + extension + var fileSize: Int? + var width: Int? // for image + var height: Int? // for image + var duration: Double? // video duration in milliseconds + var mimeType: String + var type: SharedMediaType + + init( + path: String, thumbnail: String?, fileName: String, fileSize: Int?, width: Int?, + height: Int?, duration: Double?, mimeType: String, type: SharedMediaType + ) { + self.path = path + self.thumbnail = thumbnail + self.fileName = fileName + self.fileSize = fileSize + self.width = width + self.height = height + self.duration = duration + self.mimeType = mimeType + self.type = type + } } - semaphore.wait() - return url - } - - private func decode(data: Data) -> [SharedMediaFile] { - let encodedData = try? JSONDecoder().decode([SharedMediaFile].self, from: data) - return encodedData! - } - - private func toJson(data: [SharedMediaFile]?) -> String? { - if data == nil { - return nil + + enum SharedMediaType: Int, Codable { + case image + case video + case file } - let encodedData = try? JSONEncoder().encode(data) - let json = String(data: encodedData!, encoding: .utf8)! - return json - } - - struct ShareIntentText: Codable { - let text: String - let type: String // text / weburl - } - - class SharedMediaFile: Codable { - var path: String; // can be image, video or url path - var thumbnail: String?; // video thumbnail - var fileName: String; // uuid + extension - var fileSize: Int?; - var width: Int?; // for image - var height: Int?; // for image - var duration: Double?; // video duration in milliseconds - var mimeType: String; - var type: SharedMediaType; - - init(path: String, thumbnail: String?, fileName: String, fileSize: Int?, width: Int?, height: Int?, duration: Double?, mimeType: String, type: SharedMediaType) { - self.path = path - self.thumbnail = thumbnail - self.fileName = fileName - self.fileSize = fileSize - self.width = width - self.height = height - self.duration = duration - self.mimeType = mimeType - self.type = type + + @objc + static func requiresMainQueueSetup() -> Bool { + return true } - } - - enum SharedMediaType: Int, Codable { - case image - case video - case file - } - - @objc - static func requiresMainQueueSetup() -> Bool { - return true - } } extension Encodable { - func toJSON() throws -> String? { - let jsonData = try? JSONEncoder().encode(self) - let jsonString = String(data: jsonData!, encoding: .utf8) - return jsonString - } + func toJSON() throws -> String? { + let jsonData = try? JSONEncoder().encode(self) + let jsonString = String(data: jsonData!, encoding: .utf8) + return jsonString + } } diff --git a/plugin/src/ios/ShareExtensionViewController.swift b/plugin/src/ios/ShareExtensionViewController.swift index bf6d709..0a1e209 100644 --- a/plugin/src/ios/ShareExtensionViewController.swift +++ b/plugin/src/ios/ShareExtensionViewController.swift @@ -4,12 +4,12 @@ * inspired by : * - https://ajith-ab.github.io/react-native-receive-sharing-intent/docs/ios#create-share-extension */ -import UIKit -import Social import MobileCoreServices import Photos +import Social +import UIKit -class ShareViewController: SLComposeServiceViewController { +class ShareViewController: UIViewController { let hostAppBundleIdentifier = "" let shareProtocol = "" let sharedKey = "ShareKey" @@ -19,247 +19,290 @@ class ShareViewController: SLComposeServiceViewController { let videoContentType = kUTTypeMovie as String let textContentType = kUTTypeText as String let urlContentType = kUTTypeURL as String - let fileURLType = kUTTypeFileURL as String; - - override func isContentValid() -> Bool { - return true - } - + let fileURLType = kUTTypeFileURL as String + override func viewDidLoad() { - super.viewDidLoad(); + super.viewDidLoad() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if let content = extensionContext!.inputItems[0] as? NSExtensionItem { - if let contents = content.attachments { - for (index, attachment) in (contents).enumerated() { - if attachment.hasItemConformingToTypeIdentifier(imageContentType) { - handleImages(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { - handleVideos(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { - handleFiles(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { - handleUrl(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { - handleText(content: content, attachment: attachment, index: index) - } else { - NSLog("[ERROR] content type not handle !\(String(describing: content))") - self.dismissWithError(message: "content type not handle \(String(describing: content)))") - } + Task { + guard let extensionContext = self.extensionContext, + let content = extensionContext.inputItems.first as? NSExtensionItem, + let attachments = content.attachments + else { + dismissWithError(message: "No content found") + return + } + for (index, attachment) in (attachments).enumerated() { + if attachment.hasItemConformingToTypeIdentifier(imageContentType) { + await handleImages(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { + await handleVideos(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { + await handleFiles(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { + await handleUrl(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { + await handleText(content: content, attachment: attachment, index: index) + } else { + NSLog("[ERROR] content type not handle !\(String(describing: content))") + dismissWithError(message: "content type not handle \(String(describing: content)))") } } } } - - private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in - if (error != nil) { - NSLog("[ERROR] Cannot load text content !\(String(describing: error))") - self?.dismissWithError(message: "Cannot load text content \(String(describing: error))") - } else if let item = data as? String, let this = self { - this.sharedText.append(item) - - // If this is the last item, save sharedText in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.sharedText, forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .text) + + private func handleText(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { + Task.detached { [weak self] in + if let item = try! await attachment.loadItem(forTypeIdentifier: self!.textContentType) + as? String + { + Task { @MainActor in + + self?.sharedText.append(item) + // If this is the last item, save sharedText in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(self!.hostAppBundleIdentifier)") + userDefaults?.set(self!.sharedText, forKey: self!.sharedKey) + userDefaults?.synchronize() + self?.redirectToHostApp(type: .text) + } + } - } else { NSLog("[ERROR] Cannot load text content !\(String(describing: content))") - self?.dismissWithError(message: "text content is empty \(String(describing: content)))") + await self?.dismissWithError( + message: "Cannot load text content \(String(describing: content))") } } } - - private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in - if (error != nil) { - NSLog("[ERROR] Cannot load url content !\(String(describing: error))") - self?.dismissWithError(message: "Cannot load url content \(String(describing: error))") - } else if let item = data as? URL, let this = self { - this.sharedText.append(item.absoluteString) - - // If this is the last item, save sharedText in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.sharedText, forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .weburl) + + private func handleUrl(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { + Task.detached { [weak self] in + if let item = try! await attachment.loadItem(forTypeIdentifier: self!.urlContentType) as? URL + { + Task { @MainActor in + + self!.sharedText.append(item.absoluteString) + // If this is the last item, save sharedText in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(self!.hostAppBundleIdentifier)") + userDefaults?.set(self!.sharedText, forKey: self!.sharedKey) + userDefaults?.synchronize() + self!.redirectToHostApp(type: .weburl) + } + } } else { - self?.dismissWithError(message: "url is empty") + NSLog("[ERROR] Cannot load url content !\(String(describing: content))") + await self?.dismissWithError( + message: "Cannot load url content \(String(describing: content))") } } } - - private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in - - if error == nil, let this = self { - var url: URL? = nil - if let dataURL = data as? URL { url = dataURL } - else if let imageData = data as? UIImage { url = this.saveScreenshot(imageData) } - - var pixelWidth: Int? = nil - var pixelHeight: Int? = nil - if let imageSource = CGImageSourceCreateWithURL(url! as CFURL, nil) { - if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? { - pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int - pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int - // Check orientation and flip size if required - if let orientationNumber = imageProperties[kCGImagePropertyOrientation] as! CFNumber? { - var orientation: Int = 0; - CFNumberGetValue(orientationNumber, .intType, &orientation); - if (orientation > 4) { - var temp: Int? = pixelWidth; - pixelWidth = pixelHeight; - pixelHeight = temp; + + private func handleImages(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async + { + Task.detached { [weak self] in + if let item = try? await attachment.loadItem(forTypeIdentifier: self!.imageContentType) { + Task { @MainActor in + + var url: URL? = nil + if let dataURL = item as? URL { + url = dataURL + } else if let imageData = item as? UIImage { + url = self!.saveScreenshot(imageData) + } + + var pixelWidth: Int? = nil + var pixelHeight: Int? = nil + if let imageSource = CGImageSourceCreateWithURL(url! as CFURL, nil) { + if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) + as Dictionary? + { + pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int + pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int + // Check orientation and flip size if required + if let orientationNumber = imageProperties[kCGImagePropertyOrientation] as! CFNumber? + { + var orientation: Int = 0 + CFNumberGetValue(orientationNumber, .intType, &orientation) + if orientation > 4 { + let temp: Int? = pixelWidth + pixelWidth = pixelHeight + pixelHeight = temp + } } } } - } - // Always copy - let fileName = this.getFileName(from :url!, type: .image) - let fileExtension = this.getExtension(from: url!, type: .image) - let fileSize = this.getFileSize(from: url!) - let mimeType = url!.mimeType(ext: fileExtension) - let newName = "\(UUID().uuidString).\(fileExtension)" - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(newName) - let copied = this.copyFile(at: url!, to: newPath) - if(copied) { - this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, fileName: fileName, fileSize: fileSize, width: pixelWidth, height: pixelHeight, duration: nil, mimeType: mimeType, type: .image)) - } - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .media) + // Always copy + let fileName = self!.getFileName(from: url!, type: .image) + let fileExtension = self!.getExtension(from: url!, type: .image) + let fileSize = self!.getFileSize(from: url!) + let mimeType = url!.mimeType(ext: fileExtension) + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( + forSecurityApplicationGroupIdentifier: "group.\(self!.hostAppBundleIdentifier)")! + .appendingPathComponent(newName) + let copied = self!.copyFile(at: url!, to: newPath) + if copied { + self!.sharedMedia.append( + SharedMediaFile( + path: newPath.absoluteString, thumbnail: nil, fileName: fileName, + fileSize: fileSize, width: pixelWidth, height: pixelHeight, duration: nil, + mimeType: mimeType, type: .image)) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(self!.hostAppBundleIdentifier)") + userDefaults?.set(self!.toData(data: self!.sharedMedia), forKey: self!.sharedKey) + userDefaults?.synchronize() + self!.redirectToHostApp(type: .media) + } + } - } else { - self?.dismissWithError(message: "Cannot load image content") + NSLog("[ERROR] Cannot load image content !\(String(describing: content))") + await self?.dismissWithError( + message: "Cannot load image content \(String(describing: content))") } } } - - private func documentDirectoryPath () -> URL? { + + private func documentDirectoryPath() -> URL? { let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return path.first } private func saveScreenshot(_ image: UIImage) -> URL? { var screenshotURL: URL? = nil - if let screenshotData = image.pngData(), let screenshotPath = documentDirectoryPath()?.appendingPathComponent("screenshot.png") { + if let screenshotData = image.pngData(), + let screenshotPath = documentDirectoryPath()?.appendingPathComponent("screenshot.png") + { try? screenshotData.write(to: screenshotPath) screenshotURL = screenshotPath } return screenshotURL } - private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: videoContentType, options:nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from :url, type: .video) - let fileExtension = this.getExtension(from: url, type: .video) - let fileSize = this.getFileSize(from: url) - let mimeType = url.mimeType(ext: fileExtension) - let newName = "\(UUID().uuidString).\(fileExtension)" - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(newName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - guard let sharedFile = this.getSharedMediaFile(forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) else { - return + private func handleVideos(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async + { + Task.detached { [weak self] in + if let url = try? await attachment.loadItem(forTypeIdentifier: self!.videoContentType) as? URL + { + Task { @MainActor in + + // Always copy + let fileName = self!.getFileName(from: url, type: .video) + let fileExtension = self!.getExtension(from: url, type: .video) + let fileSize = self!.getFileSize(from: url) + let mimeType = url.mimeType(ext: fileExtension) + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( + forSecurityApplicationGroupIdentifier: "group.\(self!.hostAppBundleIdentifier)")! + .appendingPathComponent(newName) + let copied = self!.copyFile(at: url, to: newPath) + if copied { + guard + let sharedFile = self!.getSharedMediaFile( + forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) + else { + return + } + self!.sharedMedia.append(sharedFile) } - this.sharedMedia.append(sharedFile) - } - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .media) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(self!.hostAppBundleIdentifier)") + userDefaults?.set(self!.toData(data: self!.sharedMedia), forKey: self!.sharedKey) + userDefaults?.synchronize() + self!.redirectToHostApp(type: .media) + } + } - } else { - self?.dismissWithError(message: "Cannot load video content") + NSLog("[ERROR] Cannot load image content !\(String(describing: content))") + await self?.dismissWithError( + message: "Cannot load image content \(String(describing: content))") } } } - - private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - // Always copy - let fileName = this.getFileName(from :url, type: .file) - let fileExtension = this.getExtension(from: url, type: .file) - let fileSize = this.getFileSize(from: url) - let mimeType = url.mimeType(ext: fileExtension) - let newName = "\(UUID().uuidString).\(fileExtension)" - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(newName) - let copied = this.copyFile(at: url, to: newPath) - if (copied) { - this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, fileName: fileName, fileSize: fileSize, width: nil, height: nil, duration: nil, mimeType: mimeType, type: .file)) - } - - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .file) + + private func handleFiles(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { + Task.detached { [weak self] in + if let url = try? await attachment.loadItem(forTypeIdentifier: self!.fileURLType) as? URL { + Task { @MainActor in + + // Always copy + let fileName = self!.getFileName(from: url, type: .file) + let fileExtension = self!.getExtension(from: url, type: .file) + let fileSize = self!.getFileSize(from: url) + let mimeType = url.mimeType(ext: fileExtension) + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( + forSecurityApplicationGroupIdentifier: "group.\(self!.hostAppBundleIdentifier)")! + .appendingPathComponent(newName) + let copied = self!.copyFile(at: url, to: newPath) + if copied { + self!.sharedMedia.append( + SharedMediaFile( + path: newPath.absoluteString, thumbnail: nil, fileName: fileName, + fileSize: fileSize, width: nil, height: nil, duration: nil, mimeType: mimeType, + type: .file)) + } + + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(self!.hostAppBundleIdentifier)") + userDefaults?.set(self!.toData(data: self!.sharedMedia), forKey: self!.sharedKey) + userDefaults?.synchronize() + self!.redirectToHostApp(type: .file) + } + } - } else { - self?.dismissWithError(message: "Cannot load file content") + NSLog("[ERROR] Cannot load video content !\(String(describing: content))") + await self?.dismissWithError( + message: "Cannot load video content \(String(describing: content))") } } } - + private func dismissWithError(message: String? = nil) { DispatchQueue.main.async { NSLog("[ERROR] Error loading application ! \(message!)") - let alert = UIAlertController(title: "Error", message: "Error loading application: \(message!)", preferredStyle: .alert) - + let alert = UIAlertController( + title: "Error", message: "Error loading application: \(message!)", preferredStyle: .alert) + let action = UIAlertAction(title: "OK", style: .cancel) { _ in self.dismiss(animated: true, completion: nil) self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } - + alert.addAction(action) self.present(alert, animated: true, completion: nil) } } - + private func redirectToHostApp(type: RedirectType) { let url = URL(string: "\(shareProtocol)://dataUrl=\(sharedKey)#\(type)")! var responder = self as UIResponder? - let selectorOpenURL = sel_registerName("openURL:") - - while (responder != nil) { + + while responder != nil { if let application = responder as? UIApplication { - if (application.canOpenURL(url)){ - application.perform(selectorOpenURL, with: url) + if application.canOpenURL(url) { + application.open(url) } else { NSLog("redirectToHostApp canOpenURL KO: \(shareProtocol)") - self.dismissWithError(message: "Application not found, invalid url scheme \(shareProtocol)") + self.dismissWithError( + message: "Application not found, invalid url scheme \(shareProtocol)") return } } @@ -267,21 +310,21 @@ class ShareViewController: SLComposeServiceViewController { } extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } - + enum RedirectType { case media case text case weburl case file } - + func getExtension(from url: URL, type: SharedMediaType) -> String { let parts = url.lastPathComponent.components(separatedBy: ".") var ex: String? = nil - if (parts.count > 1) { + if parts.count > 1 { ex = parts.last } - if (ex == nil) { + if ex == nil { switch type { case .image: ex = "PNG" @@ -293,25 +336,25 @@ class ShareViewController: SLComposeServiceViewController { } return ex ?? "Unknown" } - + func getFileName(from url: URL, type: SharedMediaType) -> String { var name = url.lastPathComponent - if (name == "") { + if name == "" { name = UUID().uuidString + "." + getExtension(from: url, type: type) } return name } - + func getFileSize(from url: URL) -> Int? { do { - let resources = try url.resourceValues(forKeys:[.fileSizeKey]) + let resources = try url.resourceValues(forKeys: [.fileSizeKey]) return resources.fileSize } catch { NSLog("Error: \(error)") return nil } } - + func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { do { if FileManager.default.fileExists(atPath: dstURL.path) { @@ -324,52 +367,65 @@ class ShareViewController: SLComposeServiceViewController { } return true } - - private func getSharedMediaFile(forVideo: URL, fileName: String, fileSize: Int?, mimeType: String) -> SharedMediaFile? { + + private func getSharedMediaFile(forVideo: URL, fileName: String, fileSize: Int?, mimeType: String) + -> SharedMediaFile? + { let asset = AVAsset(url: forVideo) let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() let thumbnailPath = getThumbnailPath(for: forVideo) - - + if FileManager.default.fileExists(atPath: thumbnailPath.path) { - return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, fileSize: fileSize, width: nil, height: nil, duration: duration, mimeType: mimeType, type: .video) + return SharedMediaFile( + path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, + fileSize: fileSize, width: nil, height: nil, duration: duration, mimeType: mimeType, + type: .video) } - + var saved = false let assetImgGenerate = AVAssetImageGenerator(asset: asset) assetImgGenerate.appliesPreferredTrackTransform = true - assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) + assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) do { - let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) + let img = try assetImgGenerate.copyCGImage( + at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) saved = true } catch { saved = false } - - return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, fileSize: fileSize, width: nil, height: nil, duration: duration, mimeType: mimeType, type: .video) : nil + + return saved + ? SharedMediaFile( + path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, + fileSize: fileSize, width: nil, height: nil, duration: duration, mimeType: mimeType, + type: .video) : nil } - + private func getThumbnailPath(for url: URL) -> URL { - let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") + let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences( + of: "==", with: "") let path = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! .appendingPathComponent("\(fileName).jpg") return path } - + class SharedMediaFile: Codable { - var path: String; // can be image, video or url path - var thumbnail: String?; // video thumbnail - var fileName: String; // uuid + extension - var fileSize: Int?; - var width: Int?; // for image - var height: Int?; // for image - var duration: Double?; // video duration in milliseconds - var mimeType: String; - var type: SharedMediaType; - - init(path: String, thumbnail: String?, fileName: String, fileSize: Int?, width: Int?, height: Int?, duration: Double?, mimeType: String, type: SharedMediaType) { + var path: String // can be image, video or url path + var thumbnail: String? // video thumbnail + var fileName: String // uuid + extension + var fileSize: Int? + var width: Int? // for image + var height: Int? // for image + var duration: Double? // video duration in milliseconds + var mimeType: String + var type: SharedMediaType + + init( + path: String, thumbnail: String?, fileName: String, fileSize: Int?, width: Int?, height: Int?, + duration: Double?, mimeType: String, type: SharedMediaType + ) { self.path = path self.thumbnail = thumbnail self.fileName = fileName @@ -381,13 +437,13 @@ class ShareViewController: SLComposeServiceViewController { self.type = type } } - + enum SharedMediaType: Int, Codable { case image case video case file } - + func toData(data: [SharedMediaFile]) -> Data { let encodedData = try? JSONEncoder().encode(data) return encodedData! @@ -395,119 +451,120 @@ class ShareViewController: SLComposeServiceViewController { } internal let mimeTypes = [ - "html": "text/html", - "htm": "text/html", - "shtml": "text/html", - "css": "text/css", - "xml": "text/xml", - "gif": "image/gif", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "js": "application/javascript", - "atom": "application/atom+xml", - "rss": "application/rss+xml", - "mml": "text/mathml", - "txt": "text/plain", - "jad": "text/vnd.sun.j2me.app-descriptor", - "wml": "text/vnd.wap.wml", - "htc": "text/x-component", - "png": "image/png", - "tif": "image/tiff", - "tiff": "image/tiff", - "wbmp": "image/vnd.wap.wbmp", - "ico": "image/x-icon", - "jng": "image/x-jng", - "bmp": "image/x-ms-bmp", - "svg": "image/svg+xml", - "svgz": "image/svg+xml", - "webp": "image/webp", - "woff": "application/font-woff", - "jar": "application/java-archive", - "war": "application/java-archive", - "ear": "application/java-archive", - "json": "application/json", - "hqx": "application/mac-binhex40", - "doc": "application/msword", - "pdf": "application/pdf", - "ps": "application/postscript", - "eps": "application/postscript", - "ai": "application/postscript", - "rtf": "application/rtf", - "m3u8": "application/vnd.apple.mpegurl", - "xls": "application/vnd.ms-excel", - "eot": "application/vnd.ms-fontobject", - "ppt": "application/vnd.ms-powerpoint", - "wmlc": "application/vnd.wap.wmlc", - "kml": "application/vnd.google-earth.kml+xml", - "kmz": "application/vnd.google-earth.kmz", - "7z": "application/x-7z-compressed", - "cco": "application/x-cocoa", - "jardiff": "application/x-java-archive-diff", - "jnlp": "application/x-java-jnlp-file", - "run": "application/x-makeself", - "pl": "application/x-perl", - "pm": "application/x-perl", - "prc": "application/x-pilot", - "pdb": "application/x-pilot", - "rar": "application/x-rar-compressed", - "rpm": "application/x-redhat-package-manager", - "sea": "application/x-sea", - "swf": "application/x-shockwave-flash", - "sit": "application/x-stuffit", - "tcl": "application/x-tcl", - "tk": "application/x-tcl", - "der": "application/x-x509-ca-cert", - "pem": "application/x-x509-ca-cert", - "crt": "application/x-x509-ca-cert", - "xpi": "application/x-xpinstall", - "xhtml": "application/xhtml+xml", - "xspf": "application/xspf+xml", - "zip": "application/zip", - "epub": "application/epub+zip", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "mid": "audio/midi", - "midi": "audio/midi", - "kar": "audio/midi", - "mp3": "audio/mpeg", - "ogg": "audio/ogg", - "m4a": "audio/x-m4a", - "ra": "audio/x-realaudio", - "3gpp": "video/3gpp", - "3gp": "video/3gpp", - "ts": "video/mp2t", - "mp4": "video/mp4", - "mpeg": "video/mpeg", - "mpg": "video/mpeg", - "mov": "video/quicktime", - "webm": "video/webm", - "flv": "video/x-flv", - "m4v": "video/x-m4v", - "mng": "video/x-mng", - "asx": "video/x-ms-asf", - "asf": "video/x-ms-asf", - "wmv": "video/x-ms-wmv", - "avi": "video/x-msvideo" + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "css": "text/css", + "xml": "text/xml", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "js": "application/javascript", + "atom": "application/atom+xml", + "rss": "application/rss+xml", + "mml": "text/mathml", + "txt": "text/plain", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "htc": "text/x-component", + "png": "image/png", + "tif": "image/tiff", + "tiff": "image/tiff", + "wbmp": "image/vnd.wap.wbmp", + "ico": "image/x-icon", + "jng": "image/x-jng", + "bmp": "image/x-ms-bmp", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "webp": "image/webp", + "woff": "application/font-woff", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "json": "application/json", + "hqx": "application/mac-binhex40", + "doc": "application/msword", + "pdf": "application/pdf", + "ps": "application/postscript", + "eps": "application/postscript", + "ai": "application/postscript", + "rtf": "application/rtf", + "m3u8": "application/vnd.apple.mpegurl", + "xls": "application/vnd.ms-excel", + "eot": "application/vnd.ms-fontobject", + "ppt": "application/vnd.ms-powerpoint", + "wmlc": "application/vnd.wap.wmlc", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "7z": "application/x-7z-compressed", + "cco": "application/x-cocoa", + "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", + "run": "application/x-makeself", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sea": "application/x-sea", + "swf": "application/x-shockwave-flash", + "sit": "application/x-stuffit", + "tcl": "application/x-tcl", + "tk": "application/x-tcl", + "der": "application/x-x509-ca-cert", + "pem": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xspf": "application/xspf+xml", + "zip": "application/zip", + "epub": "application/epub+zip", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "mp3": "audio/mpeg", + "ogg": "audio/ogg", + "m4a": "audio/x-m4a", + "ra": "audio/x-realaudio", + "3gpp": "video/3gpp", + "3gp": "video/3gpp", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "mng": "video/x-mng", + "asx": "video/x-ms-asf", + "asf": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", ] extension URL { func mimeType(ext: String?) -> String { if #available(iOSApplicationExtension 14.0, *) { if let pathExt = ext, - let mimeType = UTType(filenameExtension: pathExt)?.preferredMIMEType { + let mimeType = UTType(filenameExtension: pathExt)?.preferredMIMEType + { return mimeType } else { return "application/octet-stream" } } else { - return mimeTypes[ext?.lowercased() ?? "" ] ?? "application/octet-stream" + return mimeTypes[ext?.lowercased() ?? ""] ?? "application/octet-stream" } } } extension Array { - subscript (safe index: UInt) -> Element? { + subscript(safe index: UInt) -> Element? { return Int(index) < count ? self[Int(index)] : nil } }