add share file to TMail on iOS
This commit is contained in:
@@ -1,19 +1,37 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
import flutter_downloader
|
import flutter_downloader
|
||||||
|
import receive_sharing_intent
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
FlutterDownloaderPlugin.setPluginRegistrantCallback { registry in
|
FlutterDownloaderPlugin.setPluginRegistrantCallback { registry in
|
||||||
if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
|
if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
|
||||||
FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
|
FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
||||||
}
|
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||||
|
let sharingIntent = SwiftReceiveSharingIntentPlugin.instance
|
||||||
|
if sharingIntent.hasMatchingSchemePrefix(url: url) {
|
||||||
|
return sharingIntent.application(app, open: url, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For example load MSALPublicClientApplication
|
||||||
|
// return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String)
|
||||||
|
|
||||||
|
// Cancel url handling
|
||||||
|
// return false
|
||||||
|
|
||||||
|
// Proceed url handling for other Flutter libraries like uni_links
|
||||||
|
return super.application(app, open: url, options:options)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,22 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||||
|
<integer>15</integer>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>15</integer>
|
||||||
|
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||||
|
<integer>15</integer>
|
||||||
<key>NSExtensionActivationSupportsText</key>
|
<key>NSExtensionActivationSupportsText</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>PHSupportedMediaTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>Video</string>
|
||||||
|
<string>Image</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtensionMainStoryboard</key>
|
<key>NSExtensionMainStoryboard</key>
|
||||||
<string>MainInterface</string>
|
<string>MainInterface</string>
|
||||||
@@ -38,4 +49,4 @@
|
|||||||
<string>com.apple.share-services</string>
|
<string>com.apple.share-services</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,28 +1,42 @@
|
|||||||
//
|
|
||||||
// ShareViewController.swift
|
|
||||||
// TeamMailShareExtension
|
|
||||||
//
|
|
||||||
// Created by dab.dev on 06/04/2022.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Social
|
import Social
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
|
import Photos
|
||||||
|
|
||||||
class ShareViewController: SLComposeServiceViewController {
|
class ShareViewController: SLComposeServiceViewController {
|
||||||
|
var hostAppBundleIdentifier = ""
|
||||||
let hostAppBundleIdentifier = "com.linagora.teammail"
|
let appGroupId = "group.com.linagora.teammail"
|
||||||
let sharedKey = "ShareKey"
|
let sharedKey = "ShareKey"
|
||||||
|
var sharedMedia: [SharedMediaFile] = []
|
||||||
var sharedText: [String] = []
|
var sharedText: [String] = []
|
||||||
|
let imageContentType = kUTTypeImage as String
|
||||||
|
let videoContentType = kUTTypeMovie as String
|
||||||
let textContentType = kUTTypeText as String
|
let textContentType = kUTTypeText as String
|
||||||
let urlContentType = kUTTypeURL as String
|
let urlContentType = kUTTypeURL as String
|
||||||
|
let fileURLType = kUTTypeFileURL as String;
|
||||||
|
|
||||||
override func isContentValid() -> Bool {
|
override func isContentValid() -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadIds() {
|
||||||
|
// loading Share extension App Id
|
||||||
|
let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!;
|
||||||
|
|
||||||
|
|
||||||
|
// convert ShareExtension id to host app id
|
||||||
|
// By default it is remove last part of id after last point
|
||||||
|
// For example: com.test.ShareExtension -> com.test
|
||||||
|
let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".");
|
||||||
|
hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[..<lastIndexOfPoint!]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad();
|
super.viewDidLoad();
|
||||||
|
|
||||||
|
// load group and app id from build info
|
||||||
|
loadIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@@ -32,10 +46,16 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
|
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
|
||||||
if let contents = content.attachments {
|
if let contents = content.attachments {
|
||||||
for (index, attachment) in (contents).enumerated() {
|
for (index, attachment) in (contents).enumerated() {
|
||||||
if attachment.hasItemConformingToTypeIdentifier(textContentType) {
|
if attachment.hasItemConformingToTypeIdentifier(imageContentType) {
|
||||||
|
handleImages(content: content, attachment: attachment, index: index)
|
||||||
|
} else if attachment.hasItemConformingToTypeIdentifier(textContentType) {
|
||||||
handleText(content: content, attachment: attachment, index: index)
|
handleText(content: content, attachment: attachment, index: index)
|
||||||
|
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
|
||||||
|
handleFiles(content: content, attachment: attachment, index: index)
|
||||||
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
|
} else if attachment.hasItemConformingToTypeIdentifier(urlContentType) {
|
||||||
handleUrl(content: content, attachment: attachment, index: index)
|
handleUrl(content: content, attachment: attachment, index: index)
|
||||||
|
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
|
||||||
|
handleVideos(content: content, attachment: attachment, index: index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +80,7 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
|
|
||||||
// If this is the last item, save imagesData in userDefaults and redirect to host app
|
// If this is the last item, save imagesData in userDefaults and redirect to host app
|
||||||
if index == (content.attachments?.count)! - 1 {
|
if index == (content.attachments?.count)! - 1 {
|
||||||
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
|
let userDefaults = UserDefaults(suiteName: this.appGroupId)
|
||||||
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
|
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
|
||||||
userDefaults?.synchronize()
|
userDefaults?.synchronize()
|
||||||
this.redirectToHostApp(type: .text)
|
this.redirectToHostApp(type: .text)
|
||||||
@@ -81,7 +101,7 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
|
|
||||||
// If this is the last item, save imagesData in userDefaults and redirect to host app
|
// If this is the last item, save imagesData in userDefaults and redirect to host app
|
||||||
if index == (content.attachments?.count)! - 1 {
|
if index == (content.attachments?.count)! - 1 {
|
||||||
let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)")
|
let userDefaults = UserDefaults(suiteName: this.appGroupId)
|
||||||
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
|
userDefaults?.set(this.sharedText, forKey: this.sharedKey)
|
||||||
userDefaults?.synchronize()
|
userDefaults?.synchronize()
|
||||||
this.redirectToHostApp(type: .text)
|
this.redirectToHostApp(type: .text)
|
||||||
@@ -93,6 +113,95 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
|
||||||
|
attachment.loadItem(forTypeIdentifier: imageContentType, 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: .image)
|
||||||
|
let newPath = FileManager.default
|
||||||
|
.containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
|
||||||
|
.appendingPathComponent(fileName)
|
||||||
|
let copied = this.copyFile(at: url, to: newPath)
|
||||||
|
if(copied) {
|
||||||
|
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, 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: this.appGroupId)
|
||||||
|
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
|
||||||
|
userDefaults?.synchronize()
|
||||||
|
this.redirectToHostApp(type: .media)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self?.dismissWithError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 newPath = FileManager.default
|
||||||
|
.containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
|
||||||
|
.appendingPathComponent(fileName)
|
||||||
|
let copied = this.copyFile(at: url, to: newPath)
|
||||||
|
if(copied) {
|
||||||
|
guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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: this.appGroupId)
|
||||||
|
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
|
||||||
|
userDefaults?.synchronize()
|
||||||
|
this.redirectToHostApp(type: .media)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self?.dismissWithError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 newPath = FileManager.default
|
||||||
|
.containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
|
||||||
|
.appendingPathComponent(fileName)
|
||||||
|
let copied = this.copyFile(at: url, to: newPath)
|
||||||
|
if (copied) {
|
||||||
|
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == (content.attachments?.count)! - 1 {
|
||||||
|
let userDefaults = UserDefaults(suiteName: this.appGroupId)
|
||||||
|
userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
|
||||||
|
userDefaults?.synchronize()
|
||||||
|
this.redirectToHostApp(type: .file)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self?.dismissWithError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func dismissWithError() {
|
private func dismissWithError() {
|
||||||
print("[ERROR] Error loading data!")
|
print("[ERROR] Error loading data!")
|
||||||
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
|
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
|
||||||
@@ -107,7 +216,9 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func redirectToHostApp(type: RedirectType) {
|
private func redirectToHostApp(type: RedirectType) {
|
||||||
let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)")
|
// ids may not loaded yet so we need loadIds here too
|
||||||
|
loadIds();
|
||||||
|
let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://dataUrl=\(sharedKey)#\(type)")
|
||||||
var responder = self as UIResponder?
|
var responder = self as UIResponder?
|
||||||
let selectorOpenURL = sel_registerName("openURL:")
|
let selectorOpenURL = sel_registerName("openURL:")
|
||||||
|
|
||||||
@@ -125,6 +236,114 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
case text
|
case text
|
||||||
case file
|
case file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getExtension(from url: URL, type: SharedMediaType) -> String {
|
||||||
|
let parts = url.lastPathComponent.components(separatedBy: ".")
|
||||||
|
var ex: String? = nil
|
||||||
|
if (parts.count > 1) {
|
||||||
|
ex = parts.last
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ex == nil) {
|
||||||
|
switch type {
|
||||||
|
case .image:
|
||||||
|
ex = "PNG"
|
||||||
|
case .video:
|
||||||
|
ex = "MP4"
|
||||||
|
case .file:
|
||||||
|
ex = "TXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ex ?? "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileName(from url: URL, type: SharedMediaType) -> String {
|
||||||
|
var name = url.lastPathComponent
|
||||||
|
|
||||||
|
if (name.isEmpty) {
|
||||||
|
name = UUID().uuidString + "." + getExtension(from: url, type: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
|
||||||
|
do {
|
||||||
|
if FileManager.default.fileExists(atPath: dstURL.path) {
|
||||||
|
try FileManager.default.removeItem(at: dstURL)
|
||||||
|
}
|
||||||
|
try FileManager.default.copyItem(at: srcURL, to: dstURL)
|
||||||
|
} catch (let error) {
|
||||||
|
print("Cannot copy item at \(srcURL) to \(dstURL): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSharedMediaFile(forVideo: URL) -> 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, duration: duration, type: .video)
|
||||||
|
}
|
||||||
|
|
||||||
|
var saved = false
|
||||||
|
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||||
|
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||||
|
// let scale = UIScreen.main.scale
|
||||||
|
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
|
||||||
|
do {
|
||||||
|
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, duration: duration, type: .video) : nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getThumbnailPath(for url: URL) -> URL {
|
||||||
|
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
|
||||||
|
let path = FileManager.default
|
||||||
|
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
|
||||||
|
.appendingPathComponent("\(fileName).jpg")
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharedMediaFile: Codable {
|
||||||
|
var path: String; // can be image, video or url path. It can also be text content
|
||||||
|
var thumbnail: String?; // video thumbnail
|
||||||
|
var duration: Double?; // video duration in milliseconds
|
||||||
|
var type: SharedMediaType;
|
||||||
|
|
||||||
|
|
||||||
|
init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
|
||||||
|
self.path = path
|
||||||
|
self.thumbnail = thumbnail
|
||||||
|
self.duration = duration
|
||||||
|
self.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug method to print out SharedMediaFile details in the console
|
||||||
|
func toString() {
|
||||||
|
print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SharedMediaType: Int, Codable {
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
case file
|
||||||
|
}
|
||||||
|
|
||||||
|
func toData(data: [SharedMediaFile]) -> Data {
|
||||||
|
let encodedData = try? JSONEncoder().encode(data)
|
||||||
|
return encodedData!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array {
|
extension Array {
|
||||||
|
|||||||
+4
-1
@@ -144,7 +144,10 @@ dependencies:
|
|||||||
|
|
||||||
package_info_plus: 1.4.1
|
package_info_plus: 1.4.1
|
||||||
|
|
||||||
receive_sharing_intent: 1.4.5
|
receive_sharing_intent:
|
||||||
|
git:
|
||||||
|
url: https://github.com/ManhNTX/receive_sharing_intent.git
|
||||||
|
ref: master
|
||||||
|
|
||||||
dropdown_button2: 1.4.0
|
dropdown_button2: 1.4.0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user