diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 148073f0..316af9cc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 7A2A0C25229FF84C00FE4F1B /* common.c in Sources */ = {isa = PBXBuildFile; fileRef = 7A2A0C22229FF84C00FE4F1B /* common.c */; }; 7A2A0C26229FF84C00FE4F1B /* gitjournal.c in Sources */ = {isa = PBXBuildFile; fileRef = 7A2A0C23229FF84C00FE4F1B /* gitjournal.c */; }; 7A742348249CE14500B2C46B /* empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A742347249CE14500B2C46B /* empty.swift */; }; + 7AAD086A253F8A9F0048BC67 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAD0869253F8A9F0048BC67 /* ShareViewController.swift */; }; + 7AAD086D253F8A9F0048BC67 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AAD086B253F8A9F0048BC67 /* MainInterface.storyboard */; }; + 7AAD0871253F8A9F0048BC67 /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7AAD0867253F8A9F0048BC67 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7AD6EE48228EC97800E098B2 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7AD6EE47228EC97800E098B2 /* GoogleService-Info.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -27,7 +30,28 @@ FE285D07BE0A9C8A29B852BB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D38B87A486DD982B0932A6EF /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 7AAD086F253F8A9F0048BC67 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7AAD0866253F8A9F0048BC67; + remoteInfo = ShareExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ + 7AAD0872253F8A9F0048BC67 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 8; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 7AAD0871253F8A9F0048BC67 /* ShareExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 1; + }; 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -55,6 +79,10 @@ 7A2A0C23229FF84C00FE4F1B /* gitjournal.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = gitjournal.c; path = ../gj_common/gitjournal.c; sourceTree = ""; }; 7A742346249CE14400B2C46B /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 7A742347249CE14500B2C46B /* empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = empty.swift; sourceTree = ""; }; + 7AAD0867253F8A9F0048BC67 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AAD0869253F8A9F0048BC67 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 7AAD086C253F8A9F0048BC67 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 7AAD086E253F8A9F0048BC67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7AD6EE47228EC97800E098B2 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -73,6 +101,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 7AAD0864253F8A9F0048BC67 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -99,6 +134,16 @@ path = Pods; sourceTree = ""; }; + 7AAD0868253F8A9F0048BC67 /* ShareExtension */ = { + isa = PBXGroup; + children = ( + 7AAD0869253F8A9F0048BC67 /* ShareViewController.swift */, + 7AAD086B253F8A9F0048BC67 /* MainInterface.storyboard */, + 7AAD086E253F8A9F0048BC67 /* Info.plist */, + ); + path = ShareExtension; + sourceTree = ""; + }; 889961847ACA832709E9B420 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -132,6 +177,7 @@ 7AD6EE47228EC97800E098B2 /* GoogleService-Info.plist */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 7AAD0868253F8A9F0048BC67 /* ShareExtension */, 97C146EF1CF9000F007C117D /* Products */, 5FBA06B9450E6019CBE8E350 /* Pods */, 889961847ACA832709E9B420 /* Frameworks */, @@ -142,6 +188,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 7AAD0867253F8A9F0048BC67 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -176,6 +223,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 7AAD0866253F8A9F0048BC67 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7AAD0875253F8A9F0048BC67 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 7AAD0863253F8A9F0048BC67 /* Sources */, + 7AAD0864253F8A9F0048BC67 /* Frameworks */, + 7AAD0865253F8A9F0048BC67 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + productName = ShareExtension; + productReference = 7AAD0867253F8A9F0048BC67 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -189,10 +253,12 @@ 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 97F005F5835D02EC660945CC /* [CP] Embed Pods Frameworks */, 7AB4DD1D2346ECF500F5DF6E /* ShellScript */, + 7AAD0872253F8A9F0048BC67 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + 7AAD0870253F8A9F0048BC67 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -205,9 +271,15 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1130; LastUpgradeCheck = 1020; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { + 7AAD0866253F8A9F0048BC67 = { + CreatedOnToolsVersion = 11.3.1; + DevelopmentTeam = 4NYTN6RU3N; + ProvisioningStyle = Automatic; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = 4NYTN6RU3N; @@ -231,11 +303,20 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 7AAD0866253F8A9F0048BC67 /* ShareExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 7AAD0865253F8A9F0048BC67 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7AAD086D253F8A9F0048BC67 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -280,7 +361,8 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\n"; + shellScript = " +"; }; 7C6C1F2893FB13AF19269048 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -397,6 +479,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 7AAD0863253F8A9F0048BC67 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7AAD086A253F8A9F0048BC67 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -412,7 +502,23 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 7AAD0870253F8A9F0048BC67 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7AAD0866253F8A9F0048BC67 /* ShareExtension */; + targetProxy = 7AAD086F253F8A9F0048BC67 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ + 7AAD086B253F8A9F0048BC67 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7AAD086C253F8A9F0048BC67 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -432,6 +538,58 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 7AAD0873253F8A9F0048BC67 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4NYTN6RU3N; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.gitjournal.gitjournal.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7AAD0874253F8A9F0048BC67 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4NYTN6RU3N; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = ShareExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.gitjournal.gitjournal.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -542,6 +700,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -578,6 +737,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -612,6 +772,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 7AAD0875253F8A9F0048BC67 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7AAD0873253F8A9F0048BC67 /* Debug */, + 7AAD0874253F8A9F0048BC67 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 652377fa..18d35588 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -56,6 +56,14 @@ gitjournal + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) diff --git a/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/ios/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..286a5089 --- /dev/null +++ b/ios/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist new file mode 100644 index 00000000..695548a5 --- /dev/null +++ b/ios/ShareExtension/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ShareExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Video + Image + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/ios/ShareExtension/ShareViewController.swift b/ios/ShareExtension/ShareViewController.swift new file mode 100644 index 00000000..1d2f8ff3 --- /dev/null +++ b/ios/ShareExtension/ShareViewController.swift @@ -0,0 +1,336 @@ +// +// ShareViewController.swift +// ShareExtension +// +// Created by Vishesh Handa on 2020-10-20. +// Copyright © 2020 Vishesh Handa. All rights reserved. +// + +import UIKit +import Social +import MobileCoreServices +import Photos + +class ShareViewController: SLComposeServiceViewController { + let hostAppBundleIdentifier = "io.gitjournal.gitjournal" + let sharedKey = "ShareKey" + var sharedMedia: [SharedMediaFile] = [] + var sharedText: [String] = [] + let imageContentType = kUTTypeImage as String + 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 + } + + override func viewDidLoad() { + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + 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(textContentType) { + handleText(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(videoContentType) { + handleVideos(content: content, attachment: attachment, index: index) + } + } + } + } + } + + override func didSelectPost() { + print("didSelectPost"); + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + + private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? String, let this = self { + + this.sharedText.append(item) + + // 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.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? URL, let this = self { + + this.sharedText.append(item.absoluteString) + + // 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.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + 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: "group.\(this.hostAppBundleIdentifier)")! + .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: "group.\(this.hostAppBundleIdentifier)") + 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: "group.\(this.hostAppBundleIdentifier)")! + .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: "group.\(this.hostAppBundleIdentifier)") + 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: "group.\(this.hostAppBundleIdentifier)")! + .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: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .file) + } + + } else { + self?.dismissWithError() + } + } + } + + private func dismissWithError() { + print("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + + let action = UIAlertAction(title: "Error", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) + } + + alert.addAction(action) + present(alert, animated: true, completion: nil) + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func redirectToHostApp(type: RedirectType) { + let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)") + var responder = self as UIResponder? + let selectorOpenURL = sel_registerName("openURL:") + + while (responder != nil) { + if (responder?.responds(to: selectorOpenURL))! { + let _ = responder?.perform(selectorOpenURL, with: url) + } + responder = responder!.next + } + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + enum RedirectType { + case media + case text + 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: "group.\(hostAppBundleIdentifier)")! + .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 { + subscript (safe index: UInt) -> Element? { + return Int(index) < count ? self[Int(index)] : nil + } +} diff --git a/pubspec.lock b/pubspec.lock index 967a130d..58fa537e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -792,7 +792,7 @@ packages: name: quick_actions url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+5" + version: "0.4.0+10" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7fe036ed..b27a7550 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: image_picker: ^0.6.7 easy_localization: ^2.3.2 easy_localization_loader: ^0.0.2 - quick_actions: ^0.4.0+5 + quick_actions: ^0.4.0+10 receive_sharing_intent: ^1.4.0+2 in_app_purchase: ^0.3.4+5 flutter_plugin_android_lifecycle: ^1.0.8 # for fixing the build