TF-3250 Fix Share file from external to Twake Mail sometimes open home screen only

This commit is contained in:
dab246
2024-11-04 16:15:14 +07:00
committed by Dat H. Pham
parent e4beddd026
commit 933bb2931b
16 changed files with 268 additions and 496 deletions
-6
View File
@@ -79,12 +79,6 @@
<data android:scheme="mailto"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
+2
View File
@@ -7,4 +7,6 @@ class Constant {
static const octetStreamMimeType = 'application/octet-stream';
static const pdfExtension = '.pdf';
static const imageType = 'image';
static const textVCardMimeType = 'text/x-vcard';
static const textPlainMimeType = 'text/plain';
}
+4
View File
@@ -32,6 +32,10 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'TeamMailShareExtension' do
inherit! :search_paths
end
end
post_install do |installer|
+3 -3
View File
@@ -183,7 +183,7 @@ PODS:
- Flutter
- PromisesObjC (2.3.1)
- ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1):
- receive_sharing_intent (1.8.1):
- Flutter
- SDWebImage (5.18.10):
- SDWebImage/Core (= 5.18.10)
@@ -380,7 +380,7 @@ SPEC CHECKSUMS:
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec
SDWebImageWebPCoder: 633b813fca24f1de5e076bcd7f720c038b23892b
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -390,6 +390,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: ded4724b53568389542fa0f7d7a64c05ffc03971
PODFILE CHECKSUM: f2eb8f5a17c320a935d112236893d073c572e17e
COCOAPODS: 1.14.3
+40 -5
View File
@@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
756BBC4C51FCB44047AB39E4 /* Pods_TeamMailShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -48,7 +49,6 @@
F5BBBF512B2EEC37007930E1 /* NetworkExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF502B2EEC37007930E1 /* NetworkExceptions.swift */; };
F5BBBF532B2EECAA007930E1 /* JmapExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF522B2EECAA007930E1 /* JmapExceptions.swift */; };
F5BBBF552B2EEF3D007930E1 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */; };
F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; };
F5CFE0352B335322005A90A9 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D4EA022B2ABF090090DDFC /* NotificationService.swift */; };
F5D4EA072B2ABF090090DDFC /* TwakeMailNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F5D4EA002B2ABF090090DDFC /* TwakeMailNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -118,8 +118,11 @@
/* Begin PBXFileReference section */
06C73A54FA0478D3C89AB5E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TeamMailShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4D124E74293A67D900BA5186 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -135,6 +138,7 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.release.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.release.xcconfig"; sourceTree = "<group>"; };
F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSOTests.swift; sourceTree = "<group>"; };
F522E8852C0EE8B600DDA35B /* CoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUtils.swift; sourceTree = "<group>"; };
F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TeamMailShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -197,6 +201,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
756BBC4C51FCB44047AB39E4 /* Pods_TeamMailShareExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -223,6 +228,7 @@
isa = PBXGroup;
children = (
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */,
143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -482,6 +488,9 @@
B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */,
631346BB444C71671599207F /* Pods-Runner.release.xcconfig */,
06C73A54FA0478D3C89AB5E5 /* Pods-Runner.profile.xcconfig */,
07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */,
BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */,
1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -510,8 +519,6 @@
F5D4EA062B2ABF090090DDFC /* PBXTargetDependency */,
);
name = Runner;
packageProductDependencies = (
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@@ -520,6 +527,7 @@
isa = PBXNativeTarget;
buildConfigurationList = F52F993827FD6EB900346091 /* Build configuration list for PBXNativeTarget "TeamMailShareExtension" */;
buildPhases = (
E791D44F3C4EDE784D610B9E /* [CP] Check Pods Manifest.lock */,
F52F992927FD6EB900346091 /* Sources */,
F52F992A27FD6EB900346091 /* Frameworks */,
F52F992B27FD6EB900346091 /* Resources */,
@@ -696,7 +704,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
3D8E7FEF3D91E77326493F6D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -735,6 +743,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
E791D44F3C4EDE784D610B9E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-TeamMailShareExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -759,7 +789,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */,
F52F993027FD6EB900346091 /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1120,7 +1149,9 @@
};
F52F993927FD6EB900346091 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */;
buildSettings = {
APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1160,7 +1191,9 @@
};
F52F993A27FD6EB900346091 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */;
buildSettings = {
APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1197,7 +1230,9 @@
};
F52F993B27FD6EB900346091 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */;
buildSettings = {
APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F52F992C27FD6EB900346091"
BuildableName = "TeamMailShareExtension.appex"
BlueprintName = "TeamMailShareExtension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>${APP_GROUP_ID}</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -1,354 +1,4 @@
import UIKit
import Social
import MobileCoreServices
import Photos
import receive_sharing_intent
class ShareViewController: SLComposeServiceViewController {
var hostAppBundleIdentifier = ""
let appGroupId = "group.com.linagora.teammail"
let sharedKey = "ShareKey"
var sharedMedia: [SharedMediaFile] = []
var sharedText: [String] = []
let imageContentType = kUTTypeImage as String
let textContentType = kUTTypeText as String
let urlContentType = kUTTypeURL as String
let videoContentType = kUTTypeMovie as String
let fileURLType = kUTTypeFileURL as String
override func isContentValid() -> Bool {
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() {
super.viewDidLoad();
// load group and app id from build info
loadIds();
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 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(urlContentType) {
handleUrl(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(fileURLType) {
handleFiles(content: content, attachment: attachment, index: index)
} else if attachment.hasItemConformingToTypeIdentifier(videoContentType) {
handleVideos(content: content, attachment: attachment, index: index)
}
}
}
}
}
override func didSelectPost() {
TwakeLogger.shared.log(message: "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: this.appGroupId)
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: this.appGroupId)
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: 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() {
TwakeLogger.shared.log(message: "[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) {
// 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?
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) {
TwakeLogger.shared.log(message: "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() {
TwakeLogger.shared.log(message: "[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
}
class ShareViewController: RSIShareViewController {
}
@@ -1,22 +1,16 @@
import 'dart:io';
import 'package:core/utils/platform_info.dart';
import 'package:model/upload/file_info.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart';
extension SharedMediaFileExtension on SharedMediaFile {
File toFile() {
if (PlatformInfo.isIOS) {
final pathFile = type == SharedMediaType.FILE
? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ')
: path.toString().replaceAll('%20', ' ');
return File(pathFile);
} else {
return File(path);
}
}
File toFile() => File(path);
FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared);
FileInfo toFileInfo({bool? isShared}) =>
toFile().toFileInfo(
isInline: type == SharedMediaType.image,
isShared: isShared,
);
}
@@ -1,12 +1,7 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/email_content.dart';
import 'package:model/email/email_content_type.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/features/cleanup/domain/model/cleanup_rule.dart';
@@ -45,7 +40,7 @@ class HomeController extends ReloadableController {
void onInit() {
if (PlatformInfo.isMobile) {
_initFlutterDownloader();
_registerReceivingSharingIntent();
_registerReceivingFileSharing();
}
if (PlatformInfo.isIOS) {
_registerNotificationClickOnIOS();
@@ -85,20 +80,8 @@ class HomeController extends ReloadableController {
], eagerError: true).then((_) => getAuthenticatedAccountAction());
}
void _registerReceivingSharingIntent() {
_emailReceiveManager.receivingSharingStream.listen((uri) {
if (uri != null) {
if (GetUtils.isEmail(uri.path)) {
_emailReceiveManager.setPendingEmailAddress(EmailAddress(null, uri.path));
} else if (uri.scheme == "file") {
_emailReceiveManager.setPendingFileInfo([SharedMediaFile(uri.path, null, null, SharedMediaType.FILE)]);
} else {
_emailReceiveManager.setPendingEmailContent(EmailContent(EmailContentType.textPlain, Uri.decodeComponent(uri.path)));
}
}
});
_emailReceiveManager.receivingFileSharingStream.listen(_emailReceiveManager.setPendingFileInfo);
void _registerReceivingFileSharing() {
_emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed();
}
void _registerNotificationClickOnIOS() {
@@ -25,6 +25,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart';
import 'package:model/model.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rxdart/transformers.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
@@ -241,11 +242,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
ComposerArguments? composerArguments;
List<Identity>? _identities;
ScrollController? listSearchFilterScrollController;
late StreamSubscription _emailAddressStreamSubscription;
late StreamSubscription _emailContentStreamSubscription;
late StreamSubscription _fileReceiveManagerStreamSubscription;
StreamSubscription? _pendingSharedFileInfoSubscription;
StreamSubscription? _receivingFileSharingStreamSubscription;
StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription;
final StreamController<Either<Failure, Success>> _progressStateController =
@@ -288,6 +286,9 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
@override
void onInit() {
if (PlatformInfo.isMobile) {
_registerReceivingFileSharingStream();
}
_registerStreamListener();
BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard);
WidgetsBinding.instance.addPostFrameCallback((_) async {
@@ -301,9 +302,6 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
if (PlatformInfo.isWeb) {
listSearchFilterScrollController = ScrollController();
}
_registerPendingEmailAddress();
_registerPendingEmailContents();
_registerPendingFileInfo();
if (PlatformInfo.isIOS) {
_registerPendingCurrentEmailIdInNotification();
}
@@ -454,31 +452,85 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
}
}
void _registerPendingEmailAddress() {
_emailAddressStreamSubscription =
_emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) {
if (emailAddress?.email?.isNotEmpty == true) {
goToComposer(ComposerArguments.fromEmailAddress(emailAddress!));
}
});
void _registerReceivingFileSharingStream() {
_receivingFileSharingStreamSubscription = _emailReceiveManager
.receivingFileSharingStream
.listen(
_emailReceiveManager.setPendingFileInfo,
onError: (err) {
logError('MailboxDashBoardController::_registerReceivingFileSharingStream::receivingFileSharingStream:Exception = $err');
},
);
_pendingSharedFileInfoSubscription = _emailReceiveManager
.pendingSharedFileInfo
.listen(
_handleReceivingFileSharing,
onError: (err) {
logError('MailboxDashBoardController::_registerReceivingFileSharingStream::pendingSharedFileInfo:Exception = $err');
},
);
}
void _registerPendingEmailContents() {
_emailContentStreamSubscription =
_emailReceiveManager.pendingEmailContentInfo.stream.listen((emailContent) {
if (emailContent?.content.isNotEmpty == true) {
goToComposer(ComposerArguments.fromContentShared([emailContent!].asHtmlString));
}
});
void _handleReceivingFileSharing(List<SharedMediaFile> listSharedMediaFile) {
log('MailboxDashBoardController::_handleReceivingFileSharing: LIST_LENGTH = ${listSharedMediaFile.length}');
if (listSharedMediaFile.isEmpty) return;
for (var file in listSharedMediaFile) {
log('MailboxDashBoardController::_handleReceivingFileSharing:SharedMediaFile = ${file.toMap()}');
}
void _registerPendingFileInfo() {
_fileReceiveManagerStreamSubscription =
_emailReceiveManager.pendingFileInfo.stream.listen((listFile) {
if (listFile.isNotEmpty) {
goToComposer(ComposerArguments.fromFileShared(listFile));
if (listSharedMediaFile.length == 1) {
final sharedMediaFile = listSharedMediaFile.first;
if (sharedMediaFile.path.trim().isEmpty) return;
switch (sharedMediaFile.type) {
case SharedMediaType.image:
case SharedMediaType.video:
case SharedMediaType.file:
goToComposer(
ComposerArguments.fromFileShared([sharedMediaFile]),
);
break;
case SharedMediaType.text:
if (sharedMediaFile.mimeType == Constant.textVCardMimeType) {
goToComposer(
ComposerArguments.fromFileShared([sharedMediaFile]),
);
} else if (sharedMediaFile.mimeType == Constant.textPlainMimeType) {
goToComposer(
ComposerArguments.fromContentShared(sharedMediaFile.path.trim()),
);
}
});
break;
case SharedMediaType.url:
if (sharedMediaFile.path.startsWith(RouteUtils.mailtoPrefix)) {
final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(sharedMediaFile.path);
goToComposer(
ComposerArguments.fromMailtoUri(
listEmailAddress: navigationRouter.listEmailAddress,
subject: navigationRouter.subject,
body: navigationRouter.body,
),
);
}
break;
case SharedMediaType.mailto:
if (EmailUtils.isEmailAddressValid(sharedMediaFile.path)) {
goToComposer(
ComposerArguments.fromEmailAddress(
EmailAddress(null, sharedMediaFile.path),
),
);
}
break;
}
return;
}
goToComposer(
ComposerArguments.fromFileShared(listSharedMediaFile),
);
}
void _registerPendingCurrentEmailIdInNotification() {
@@ -2861,14 +2913,15 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
if (PlatformInfo.isWeb) {
listSearchFilterScrollController?.dispose();
}
_emailReceiveManager.closeEmailReceiveManagerStream();
if (PlatformInfo.isIOS) {
_iosNotificationManager?.dispose();
_currentEmailIdInNotificationIOSStreamSubscription?.cancel();
}
_emailAddressStreamSubscription.cancel();
_emailContentStreamSubscription.cancel();
_fileReceiveManagerStreamSubscription.cancel();
if (PlatformInfo.isMobile) {
_pendingSharedFileInfoSubscription?.cancel();
_receivingFileSharingStreamSubscription?.cancel();
_emailReceiveManager.closeEmailReceiveManagerStream();
}
_progressStateController.close();
_refreshActionEventController.close();
_notificationManager.closeStream();
+17 -54
View File
@@ -1,75 +1,38 @@
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/email_content.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rxdart/rxdart.dart';
class EmailReceiveManager {
BehaviorSubject<List<SharedMediaFile>> _pendingSharedFileInfo =
BehaviorSubject.seeded(List.empty(growable: true));
BehaviorSubject<List<SharedMediaFile>> get pendingSharedFileInfo =>
_pendingSharedFileInfo;
BehaviorSubject<EmailAddress?> _pendingEmailAddressInfo = BehaviorSubject.seeded(null);
BehaviorSubject<EmailAddress?> get pendingEmailAddressInfo => _pendingEmailAddressInfo;
Stream<List<SharedMediaFile>> get receivingFileSharingStream =>
ReceiveSharingIntent.instance.getMediaStream();
BehaviorSubject<EmailContent?> _pendingEmailContentInfo = BehaviorSubject.seeded(null);
BehaviorSubject<EmailContent?> get pendingEmailContentInfo => _pendingEmailContentInfo;
void registerReceivingFileSharingStreamWhileAppClosed() {
ReceiveSharingIntent.instance.getInitialMedia().then((value) {
setPendingFileInfo(value);
BehaviorSubject<List<SharedMediaFile>> _pendingFileInfo = BehaviorSubject.seeded(List.empty(growable: true));
BehaviorSubject<List<SharedMediaFile>> get pendingFileInfo => _pendingFileInfo;
Stream<Uri?> get receivingSharingStream {
return Rx.merge([
Stream.fromFuture(ReceiveSharingIntent.getInitialTextAsUri()),
ReceiveSharingIntent.getTextStreamAsUri()
]);
}
Stream<List<SharedMediaFile>> get receivingFileSharingStream {
return Rx.merge([
Stream.fromFuture(ReceiveSharingIntent.getInitialMedia()),
ReceiveSharingIntent.getMediaStream()
]);
}
void setPendingEmailAddress(EmailAddress emailAddress) async {
_clearPendingEmailAddress();
_pendingEmailAddressInfo.add(emailAddress);
}
void setPendingEmailContent(EmailContent emailContent) async {
_clearPendingEmailContent();
_pendingEmailContentInfo.add(emailContent);
}
void _clearPendingEmailContent() {
if (_pendingEmailContentInfo.isClosed) {
_pendingEmailContentInfo = BehaviorSubject.seeded(null);
} else {
_pendingEmailContentInfo.add(null);
}
}
void _clearPendingEmailAddress() {
if(_pendingEmailAddressInfo.isClosed) {
_pendingEmailAddressInfo = BehaviorSubject.seeded(null);
} else {
_pendingEmailAddressInfo.add(null);
}
ReceiveSharingIntent.instance.reset();
});
}
void closeEmailReceiveManagerStream() {
_pendingEmailAddressInfo.close();
_pendingEmailContentInfo.close();
_pendingFileInfo.close();
_pendingSharedFileInfo.close();
}
void setPendingFileInfo(List<SharedMediaFile> list) async {
void setPendingFileInfo(List<SharedMediaFile> list) {
_clearPendingFileInfo();
_pendingFileInfo.add(list);
_pendingSharedFileInfo.add(list);
}
void _clearPendingFileInfo() {
if(_pendingFileInfo.isClosed) {
_pendingFileInfo = BehaviorSubject.seeded(List.empty(growable: true));
if(_pendingSharedFileInfo.isClosed) {
_pendingSharedFileInfo = BehaviorSubject.seeded(List.empty(growable: true));
} else {
_pendingFileInfo.add(List.empty(growable: true));
_pendingSharedFileInfo.add(List.empty(growable: true));
}
}
}
+3 -3
View File
@@ -1758,11 +1758,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: master
resolved-ref: f23f7fb0fad25ae80a88350ad8654851f1ee682f
ref: main
resolved-ref: b388eb837bd1256a9623a8cd0a62cfc21e9f51cf
url: "https://github.com/linagora/receive_sharing_intent.git"
source: git
version: "1.4.5"
version: "1.8.1"
rich_text_composer:
dependency: "direct main"
description:
+1 -1
View File
@@ -75,7 +75,7 @@ dependencies:
receive_sharing_intent:
git:
url: https://github.com/linagora/receive_sharing_intent.git
ref: master
ref: main
flutter_appauth_web:
git:
@@ -347,9 +347,7 @@ void main() {
setUp(() {
getEmailsInMailboxInteractor = MockGetEmailsInMailboxInteractor();
when(emailReceiveManager.pendingEmailAddressInfo).thenAnswer((_) => BehaviorSubject.seeded(null));
when(emailReceiveManager.pendingEmailContentInfo).thenAnswer((_) => BehaviorSubject.seeded(null));
when(emailReceiveManager.pendingFileInfo).thenAnswer((_) => BehaviorSubject.seeded([]));
when(emailReceiveManager.pendingSharedFileInfo).thenAnswer((_) => BehaviorSubject.seeded([]));
Get.put(mailboxDashboardController);
mailboxDashboardController.onReady();
@@ -306,9 +306,7 @@ void main() {
Get.put<GetAllIdentitiesInteractor>(getAllIdentitiesInteractor);
Get.put<RemoveComposerCacheOnWebInteractor>(removeComposerCacheOnWebInteractor);
when(emailReceiveManager.pendingEmailAddressInfo).thenAnswer((_) => BehaviorSubject.seeded(null));
when(emailReceiveManager.pendingEmailContentInfo).thenAnswer((_) => BehaviorSubject.seeded(null));
when(emailReceiveManager.pendingFileInfo).thenAnswer((_) => BehaviorSubject.seeded([]));
when(emailReceiveManager.pendingSharedFileInfo).thenAnswer((_) => BehaviorSubject.seeded([]));
searchController = SearchController(
quickSearchEmailInteractor,