diff --git a/Podfile b/Podfile index 304ee31d3..15cf061bb 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,6 @@ source 'https://github.com/CocoaPods/Specs.git' target "Riot" do - # Different flavours of pods to MatrixKit # The tagged version on which this version of Riot has been built pod 'MatrixKit', '0.6.3' @@ -14,8 +13,8 @@ pod 'MatrixKit', '0.6.3' #pod 'MatrixKit' # The develop branch version -#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'develop' -#pod 'MatrixKit', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'develop' +#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'callkit' +#pod 'MatrixKit', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'callkit' # The one used for developing both MatrixSDK and MatrixKit # Note that MatrixSDK must be cloned into a folder called matrix-ios-sdk next to the MatrixKit folder @@ -59,8 +58,8 @@ pod 'MatrixKit/AppExtension', '0.6.3' #pod 'MatrixKit/AppExtension' # The develop branch version -#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'develop' -#pod 'MatrixKit/AppExtension', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'develop' +#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'callkit' +#pod 'MatrixKit/AppExtension', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'callkit' # The one used for developing both MatrixSDK and MatrixKit # Note that MatrixSDK must be cloned into a folder called matrix-ios-sdk next to the MatrixKit folder @@ -74,5 +73,34 @@ pod 'cmark', :inhibit_warnings => true end +target "SiriIntents" do +pod 'GoogleAnalytics' +# The Google WebRTC stack +pod 'WebRTC', '58.17.16937' +# OLMKit for crypto +pod 'OLMKit' +#pod 'OLMKit', :path => '../olm/OLMKit.podspec' +pod 'Realm', '~> 2.10.2' + +# The tagged version on which this version of Riot share extension has been built +pod 'MatrixKit/AppExtension', '0.6.3' + +# The lastest release available on the CocoaPods repository +#pod 'MatrixKit/AppExtension' + +# The develop branch version +#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'callkit' +#pod 'MatrixKit/AppExtension', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'callkit' + +# The one used for developing both MatrixSDK and MatrixKit +# Note that MatrixSDK must be cloned into a folder called matrix-ios-sdk next to the MatrixKit folder +#pod 'MatrixSDK', :path => '../matrix-ios-sdk/MatrixSDK.podspec' +#pod 'MatrixKit/AppExtension', :path => '../matrix-ios-kit/MatrixKit.podspec' + +# Remove warnings from "bad" pods +pod 'OLMKit', :inhibit_warnings => true +pod 'cmark', :inhibit_warnings => true + +end diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 36732053c..49981a658 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -84,9 +84,14 @@ 32F3AE1A1F6FF4E600F0F004 /* WidgetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32F3AE191F6FF4E600F0F004 /* WidgetViewController.m */; }; 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */; }; 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */; }; + 714F6391AC0AA86C0AEB3F43 /* libPods-SiriIntents.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */; }; 83711A7C1F6F8E7D008F0D4D /* KeyboardGrowingTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 83711A7B1F6F8E7D008F0D4D /* KeyboardGrowingTextView.m */; }; 92324BE31F4F66D3009DE194 /* IncomingCallView.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE21F4F66D3009DE194 /* IncomingCallView.m */; }; 92324BE61F4F6A60009DE194 /* CircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE51F4F6A60009DE194 /* CircleButton.m */; }; + 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */; }; + 92726A471F58737A004AD26F /* IntentHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 92726A461F58737A004AD26F /* IntentHandler.m */; }; + 92726A4B1F58737A004AD26F /* SiriIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 92726A431F58737A004AD26F /* SiriIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 92726A511F587410004AD26F /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92726A501F587410004AD26F /* Intents.framework */; }; A27ECCE3FC4971745D2CB78D /* libPods-RiotShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */; }; F0131DE51F2200D600CBF707 /* RiotSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */; }; F02C1A861E8EB04C0045A404 /* PeopleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F02C1A841E8EB04C0045A404 /* PeopleViewController.m */; }; @@ -555,6 +560,13 @@ remoteGlobalIDString = 24CBEC4D1F0EAD310093EABB; remoteInfo = "Riot Share Extension"; }; + 92726A491F58737A004AD26F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F094A99A1B78D8F000B1FBBF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 92726A421F58737A004AD26F; + remoteInfo = SiriIntents; + }; F094A9BF1B78D8F000B1FBBF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F094A99A1B78D8F000B1FBBF /* Project object */; @@ -572,6 +584,7 @@ dstSubfolderSpec = 13; files = ( 24CBEC591F0EAD310093EABB /* RiotShareExtension.appex in Embed App Extensions */, + 92726A4B1F58737A004AD26F /* SiriIntents.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -678,6 +691,9 @@ 32FD0A3A1EB0CD9B0072B066 /* BugReportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugReportViewController.h; sourceTree = ""; }; 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugReportViewController.m; sourceTree = ""; }; 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BugReportViewController.xib; sourceTree = ""; }; + 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SiriIntents.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents.debug.xcconfig"; sourceTree = ""; }; + 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SiriIntents.release.xcconfig"; path = "Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents.release.xcconfig"; sourceTree = ""; }; + 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SiriIntents.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RiotShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 765F5104DB3EC39713DEB3A4 /* Pods-RiotShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotShareExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-RiotShareExtension/Pods-RiotShareExtension.release.xcconfig"; sourceTree = ""; }; 83711A7B1F6F8E7D008F0D4D /* KeyboardGrowingTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeyboardGrowingTextView.m; sourceTree = ""; }; @@ -686,6 +702,14 @@ 92324BE21F4F66D3009DE194 /* IncomingCallView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IncomingCallView.m; sourceTree = ""; }; 92324BE41F4F6A60009DE194 /* CircleButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleButton.h; sourceTree = ""; }; 92324BE51F4F6A60009DE194 /* CircleButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleButton.m; sourceTree = ""; }; + 926FA53D1F4C132000F826C2 /* MXSession+Riot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MXSession+Riot.h"; sourceTree = ""; }; + 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MXSession+Riot.m"; sourceTree = ""; }; + 92726A431F58737A004AD26F /* SiriIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SiriIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 92726A451F58737A004AD26F /* IntentHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IntentHandler.h; sourceTree = ""; }; + 92726A461F58737A004AD26F /* IntentHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IntentHandler.m; sourceTree = ""; }; + 92726A481F58737A004AD26F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 92726A4F1F587393004AD26F /* SiriIntents.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SiriIntents.entitlements; sourceTree = ""; }; + 92726A501F587410004AD26F /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; C195C53961EA28E6900AEB68 /* Pods-Riot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.release.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.release.xcconfig"; sourceTree = ""; }; F0131DE31F2200D600CBF707 /* RiotSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RiotSplitViewController.h; sourceTree = ""; }; F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RiotSplitViewController.m; sourceTree = ""; }; @@ -1280,6 +1304,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 92726A401F58737A004AD26F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 92726A511F587410004AD26F /* Intents.framework in Frameworks */, + 714F6391AC0AA86C0AEB3F43 /* libPods-SiriIntents.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F094A99F1B78D8F000B1FBBF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1470,8 +1503,10 @@ 5FC42FA41F5186AFFB6A2404 /* Frameworks */ = { isa = PBXGroup; children = ( + 92726A501F587410004AD26F /* Intents.framework */, FD9D0BDE9232898950554DD5 /* libPods-Riot.a */, 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */, + 5666C1236223F54D4C635C54 /* libPods-SiriIntents.a */, ); name = Frameworks; sourceTree = ""; @@ -1483,6 +1518,8 @@ C195C53961EA28E6900AEB68 /* Pods-Riot.release.xcconfig */, 12AA0005C8B3D8D8162584C5 /* Pods-RiotShareExtension.debug.xcconfig */, 765F5104DB3EC39713DEB3A4 /* Pods-RiotShareExtension.release.xcconfig */, + 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */, + 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -1498,6 +1535,17 @@ path = Calls; sourceTree = ""; }; + 92726A441F58737A004AD26F /* SiriIntents */ = { + isa = PBXGroup; + children = ( + 92726A4F1F587393004AD26F /* SiriIntents.entitlements */, + 92726A451F58737A004AD26F /* IntentHandler.h */, + 92726A461F58737A004AD26F /* IntentHandler.m */, + 92726A481F58737A004AD26F /* Info.plist */, + ); + path = SiriIntents; + sourceTree = ""; + }; F04AF25E1F83A4C000D20F4D /* zh_Hans.lproj */ = { isa = PBXGroup; children = ( @@ -1850,6 +1898,8 @@ F083BBEA1E7009EC00A9B29C /* UINavigationController+Riot.m */, F083BBEB1E7009EC00A9B29C /* UIViewController+RiotSearch.h */, F083BBEC1E7009EC00A9B29C /* UIViewController+RiotSearch.m */, + 926FA53D1F4C132000F826C2 /* MXSession+Riot.h */, + 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */, ); path = Categories; sourceTree = ""; @@ -2386,6 +2436,7 @@ F083BB081E7009EC00A9B29C /* Riot */, F083BB021E7005FD00A9B29C /* RiotTests */, 24CBEC4F1F0EAD310093EABB /* RiotShareExtension */, + 92726A441F58737A004AD26F /* SiriIntents */, F094A9A31B78D8F000B1FBBF /* Products */, 7471DF3720D498384A068DA7 /* Pods */, 5FC42FA41F5186AFFB6A2404 /* Frameworks */, @@ -2398,6 +2449,7 @@ F094A9A21B78D8F000B1FBBF /* Riot.app */, F094A9BE1B78D8F000B1FBBF /* RiotTests.xctest */, 24CBEC4E1F0EAD310093EABB /* RiotShareExtension.appex */, + 92726A431F58737A004AD26F /* SiriIntents.appex */, ); name = Products; sourceTree = ""; @@ -2434,6 +2486,25 @@ productReference = 24CBEC4E1F0EAD310093EABB /* RiotShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 92726A421F58737A004AD26F /* SiriIntents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 92726A4E1F58737A004AD26F /* Build configuration list for PBXNativeTarget "SiriIntents" */; + buildPhases = ( + 6AA0024D4D5FAE30C2E1F311 /* [CP] Check Pods Manifest.lock */, + 92726A3F1F58737A004AD26F /* Sources */, + 92726A401F58737A004AD26F /* Frameworks */, + 92726A411F58737A004AD26F /* Resources */, + 807A0ABF153A23C2FC22F977 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SiriIntents; + productName = SiriIntents; + productReference = 92726A431F58737A004AD26F /* SiriIntents.appex */; + productType = "com.apple.product-type.app-extension"; + }; F094A9A11B78D8F000B1FBBF /* Riot */ = { isa = PBXNativeTarget; buildConfigurationList = F094A9C81B78D8F000B1FBBF /* Build configuration list for PBXNativeTarget "Riot" */; @@ -2451,6 +2522,7 @@ ); dependencies = ( 242661F61F12B1BA00D3FC08 /* PBXTargetDependency */, + 92726A4A1F58737A004AD26F /* PBXTargetDependency */, ); name = Riot; productName = Vector; @@ -2494,6 +2566,16 @@ }; }; }; + 92726A421F58737A004AD26F = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = 7J4U792NQT; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; F094A9A11B78D8F000B1FBBF = { CreatedOnToolsVersion = 6.2; DevelopmentTeam = 7J4U792NQT; @@ -2536,6 +2618,7 @@ F094A9A11B78D8F000B1FBBF /* Riot */, F094A9BD1B78D8F000B1FBBF /* RiotTests */, 24CBEC4D1F0EAD310093EABB /* RiotShareExtension */, + 92726A421F58737A004AD26F /* SiriIntents */, ); }; /* End PBXProject section */ @@ -2565,6 +2648,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 92726A411F58737A004AD26F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F094A9A01B78D8F000B1FBBF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3053,6 +3143,24 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 6AA0024D4D5FAE30C2E1F311 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SiriIntents-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; + }; 7FFD40AA75DB32D83350D225 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3071,6 +3179,24 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Riot/Pods-Riot-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 807A0ABF153A23C2FC22F977 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents-resources.sh", + "${PODS_ROOT}/MatrixSDK/MatrixSDK/Data/Store/MXCoreDataStore/MXCoreDataStore.xcdatamodeld", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SiriIntents/Pods-SiriIntents-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 8EA19F5011654D3BD5EDAC33 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3113,6 +3239,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 92726A3F1F58737A004AD26F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 92726A471F58737A004AD26F /* IntentHandler.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F094A99E1B78D8F000B1FBBF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3238,6 +3372,7 @@ F083BDF81E7009ED00A9B29C /* RoomDataSource.m in Sources */, F083BE371E7009ED00A9B29C /* RoomActivitiesView.m in Sources */, F083BE131E7009ED00A9B29C /* HomeMessagesSearchViewController.m in Sources */, + 926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */, F083BE8C1E7009ED00A9B29C /* PreviewRoomTitleView.m in Sources */, F083BE271E7009ED00A9B29C /* SettingsViewController.m in Sources */, F083BE9A1E7009ED00A9B29C /* TableViewCellWithButton.m in Sources */, @@ -3282,6 +3417,11 @@ target = 24CBEC4D1F0EAD310093EABB /* RiotShareExtension */; targetProxy = 242661F51F12B1BA00D3FC08 /* PBXContainerItemProxy */; }; + 92726A4A1F58737A004AD26F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 92726A421F58737A004AD26F /* SiriIntents */; + targetProxy = 92726A491F58737A004AD26F /* PBXContainerItemProxy */; + }; F094A9C01B78D8F000B1FBBF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F094A9A11B78D8F000B1FBBF /* Riot */; @@ -3506,6 +3646,47 @@ }; name = Release; }; + 92726A4C1F58737A004AD26F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 397BCA987893439918EBF330 /* Pods-SiriIntents.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = SiriIntents/SiriIntents.entitlements; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7J4U792NQT; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.SiriIntents; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 92726A4D1F58737A004AD26F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4D1164C2F07EF74950DCDA7A /* Pods-SiriIntents.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = SiriIntents/SiriIntents.entitlements; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7J4U792NQT; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = im.vector.app.SiriIntents; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; F094A9C61B78D8F000B1FBBF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3686,6 +3867,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 92726A4E1F58737A004AD26F /* Build configuration list for PBXNativeTarget "SiriIntents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92726A4C1F58737A004AD26F /* Debug */, + 92726A4D1F58737A004AD26F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F094A99D1B78D8F000B1FBBF /* Build configuration list for PBXProject "Riot" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Riot/AppDelegate.h b/Riot/AppDelegate.h index dd33b3579..55b3cd54e 100644 --- a/Riot/AppDelegate.h +++ b/Riot/AppDelegate.h @@ -43,7 +43,7 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification; @interface AppDelegate : UIResponder { - BOOL isAPNSRegistered; + BOOL isPushRegistered; // background sync management void (^_completionHandler)(UIBackgroundFetchResult); @@ -112,7 +112,7 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification; - (void)startGoogleAnalytics; - (void)stopGoogleAnalytics; -#pragma mark - APNS methods +#pragma mark - Push notifications - (void)registerUserNotificationSettings; diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index eedba7c26..a397fa6c7 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -17,6 +17,9 @@ #import "AppDelegate.h" +#import +#import + #import "RecentsDataSource.h" #import "RoomDataSource.h" @@ -40,8 +43,16 @@ #import +#include + +// Calls #import "CallViewController.h" +#import +#import + +#import "MXSession+Riot.h" + //#define MX_CALL_STACK_OPENWEBRTC #ifdef MX_CALL_STACK_OPENWEBRTC #import @@ -51,9 +62,10 @@ #import #endif -#include - -#include +#ifdef MX_CALL_STACK_JINGLE +#import +#import +#endif #define CALL_STATUS_BAR_HEIGHT 44 @@ -63,7 +75,7 @@ NSString *const kAppDelegateDidTapStatusBarNotification = @"kAppDelegateDidTapStatusBarNotification"; NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateNetworkStatusDidChangeNotification"; -@interface AppDelegate () +@interface AppDelegate () { /** Reachability observer @@ -106,11 +118,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN */ NSMutableArray *mxSessionArray; - /** - The room id of the current handled remote notification (if any) - */ - NSString *remoteNotificationRoomId; - /** The fragment of the universal link being processing. Only one fragment is handled at a time. @@ -147,6 +154,13 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN */ NSMutableDictionary *callEventsListeners; + /** + The notification listener blocks. + There is one block per MXSession. + The key is an identifier of the MXSession. The value, the listener block. + */ + NSMutableDictionary *notificationListenerBlocks; + /** Currently displayed "Call not supported" alert. */ @@ -165,9 +179,14 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } @property (strong, nonatomic) UIAlertController *mxInAppNotification; +@property (strong, nonatomic) UIAlertController *incomingCallNotification; @property (nonatomic, nullable, copy) void (^registrationForRemoteNotificationsCompletion)(NSError *); + +@property (nonatomic, strong) PKPushRegistry *pushRegistry; +@property (nonatomic) BOOL hasPendingLocalNotifications; + @end @implementation AppDelegate @@ -307,6 +326,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN mxSessionArray = [NSMutableArray array]; callEventsListeners = [NSMutableDictionary dictionary]; + notificationListenerBlocks = [NSMutableDictionary dictionary]; // To simplify navigation into the app, we retrieve here the main navigation controller and the tab bar controller. UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; @@ -443,13 +463,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - // cancel any background sync before resuming - // i.e. warn IOS that there is no new data with any received push. - [self cancelBackgroundSync]; - - // Open account session(s) if this is not already done (see [initMatrixSessions] in case of background launch). - [self prepareSessionForActiveAccounts]; - _isAppForeground = YES; // GA: Start a new session. The next hit from this tracker will be the first in a new session. @@ -460,7 +473,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { NSLog(@"[AppDelegate] applicationDidBecomeActive"); - remoteNotificationRoomId = nil; + _hasPendingLocalNotifications = NO; // Check if there is crash log to send if ([[NSUserDefaults standardUserDefaults] boolForKey:@"enableCrashReport"]) @@ -547,6 +560,67 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { continueUserActivity = [self handleUniversalLink:userActivity]; } + else if ([userActivity.activityType isEqualToString:INStartAudioCallIntentIdentifier] || + [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]) + { + INInteraction *interaction = userActivity.interaction; + + // roomID provided by Siri intent + NSString *roomID = userActivity.userInfo[@"roomID"]; + + // We've launched from calls history list + if (!roomID) + { + INPerson *person; + + if ([interaction.intent isKindOfClass:INStartAudioCallIntent.class]) + { + person = [[(INStartAudioCallIntent *)(interaction.intent) contacts] firstObject]; + } + else if ([interaction.intent isKindOfClass:INStartVideoCallIntent.class]) + { + person = [[(INStartVideoCallIntent *)(interaction.intent) contacts] firstObject]; + } + + roomID = person.personHandle.value; + } + + BOOL isVideoCall = [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]; + + UIApplication *application = UIApplication.sharedApplication; + NSNumber *backgroundTaskIdentifier; + + // Start background task since we need time for MXSession preparasion because our app can be launched in the background + if (application.applicationState == UIApplicationStateBackground) + backgroundTaskIdentifier = @([application beginBackgroundTaskWithExpirationHandler:^{}]); + + MXSession *session = mxSessionArray.firstObject; + [session.callManager placeCallInRoom:roomID + withVideo:isVideoCall + success:^(MXCall *call) { + if (application.applicationState == UIApplicationStateBackground) + { + __weak NSNotificationCenter *center = NSNotificationCenter.defaultCenter; + __block id token = + [center addObserverForName:kMXCallStateDidChange + object:call + queue:nil + usingBlock:^(NSNotification * _Nonnull note) { + if (call.state == MXCallStateEnded) + { + [application endBackgroundTask:backgroundTaskIdentifier.unsignedIntegerValue]; + [center removeObserver:token]; + } + }]; + } + } + failure:^(NSError *error) { + if (backgroundTaskIdentifier) + [application endBackgroundTask:backgroundTaskIdentifier.unsignedIntegerValue]; + }]; + + continueUserActivity = YES; + } return continueUserActivity; } @@ -890,11 +964,11 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } -#pragma mark - APNS methods +#pragma mark - Push notifications - (void)registerUserNotificationSettings { - if (!isAPNSRegistered) + if (!isPushRegistered) { // Registration on iOS 8 and later UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeBadge | UIUserNotificationTypeSound |UIUserNotificationTypeAlert) categories:nil]; @@ -905,7 +979,10 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)registerForRemoteNotificationsWithCompletion:(nullable void (^)(NSError *))completion { self.registrationForRemoteNotificationsCompletion = completion; - [[UIApplication sharedApplication] registerForRemoteNotifications]; + + self.pushRegistry = [[PKPushRegistry alloc] initWithQueue:nil]; + self.pushRegistry.delegate = self; + self.pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; } - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings @@ -919,58 +996,15 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { // Clear existing token MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; - [accountManager setApnsDeviceToken:nil]; + [accountManager setPushDeviceToken:nil withPushOptions:nil]; } } -- (void)application:(UIApplication*)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { - NSUInteger len = ((deviceToken.length > 8) ? 8 : deviceToken.length / 2); - NSLog(@"[AppDelegate] Got APNS token! (%@ ...)", [deviceToken subdataWithRange:NSMakeRange(0, len)]); + NSLog(@"[AppDelegate] didReceiveLocalNotification: applicationState: %@", @([UIApplication sharedApplication].applicationState)); - MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; - [accountManager setApnsDeviceToken:deviceToken]; - - isAPNSRegistered = YES; - - if (self.registrationForRemoteNotificationsCompletion) - { - self.registrationForRemoteNotificationsCompletion(nil); - self.registrationForRemoteNotificationsCompletion = nil; - } -} - -- (void)application:(UIApplication*)app didFailToRegisterForRemoteNotificationsWithError:(NSError*)error -{ - NSLog(@"[AppDelegate] Failed to register for APNS: %@", error); - - if (self.registrationForRemoteNotificationsCompletion) - { - self.registrationForRemoteNotificationsCompletion(error); - self.registrationForRemoteNotificationsCompletion = nil; - } -} - -- (void)cancelBackgroundSync -{ - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNoData); - _completionHandler = nil; - } -} - -- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ - NSLog(@"[AppDelegate] didReceiveRemoteNotification: applicationState: %@", @([UIApplication sharedApplication].applicationState)); - -#ifdef DEBUG - // log the full userInfo only in DEBUG - NSLog(@"[AppDelegate] didReceiveRemoteNotification: %@", userInfo); -#endif - - // Look for the room id - NSString* roomId = [userInfo objectForKey:@"room_id"]; + NSString* roomId = notification.userInfo[@"room_id"]; if (roomId.length) { // TODO retrieve the right matrix session @@ -1000,82 +1034,155 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // sanity checks if (dedicatedAccount && dedicatedAccount.mxSession) { - UIApplicationState state = [UIApplication sharedApplication].applicationState; + NSLog(@"[AppDelegate] didReceiveLocalNotification: open the roomViewController %@", roomId); - // Jump to the concerned room only if the app is transitioning from the background - if (state == UIApplicationStateInactive) - { - // Check whether another remote notification is not already processed - if (!remoteNotificationRoomId) - { - remoteNotificationRoomId = roomId; - - NSLog(@"[AppDelegate] didReceiveRemoteNotification: open the roomViewController %@", roomId); - - [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession]; - } - else - { - NSLog(@"[AppDelegate] didReceiveRemoteNotification: busy"); - } - } - else if (!_completionHandler && (state == UIApplicationStateBackground)) - { - _completionHandler = completionHandler; - - NSLog(@"[AppDelegate] didReceiveRemoteNotification: starts a background sync"); - - [dedicatedAccount backgroundSync:20000 success:^{ - NSLog(@"[AppDelegate] didReceiveRemoteNotification: the background sync succeeds"); - - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNewData); - _completionHandler = nil; - } - } failure:^(NSError *error) { - NSLog(@"[AppDelegate] didReceiveRemoteNotification: the background sync fails"); - - if (_completionHandler) - { - _completionHandler(UIBackgroundFetchResultNoData); - _completionHandler = nil; - } - }]; - - // wait that the background sync is done - return; - } + [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession]; } else { - NSLog(@"[AppDelegate] didReceiveRemoteNotification : no linked session / account has been found."); + NSLog(@"[AppDelegate] didReceiveLocalNotification : no linked session / account has been found."); } } - - // Possible workaround for https://github.com/vector-im/riot-ios/issues/1522 - // iOS 11 ignores us when we say that we will not display a local notification - // making the app crash in background. - // So, try to send the information a bit later with a dispatch_async :/ - NSLog(@"[AppDelegate] didReceiveRemoteNotification: BEFORE completionHandler #1"); - dispatch_async(dispatch_get_main_queue(), ^{ - - NSLog(@"[AppDelegate] didReceiveRemoteNotification: BEFORE completionHandler #2"); - completionHandler(UIBackgroundFetchResultNoData); - NSLog(@"[AppDelegate] didReceiveRemoteNotification: AFTER completionHandler"); - }); } -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type { - // iOS 10 (at least up to GM beta release) does not call application:didReceiveRemoteNotification:fetchCompletionHandler: - // when the user clicks on a notification but it calls this deprecated version - // of didReceiveRemoteNotification. - // Use this method as a workaround as adviced at http://stackoverflow.com/a/39419245 - NSLog(@"[AppDelegate] didReceiveRemoteNotification (deprecated version)"); + NSData *token = credentials.token; - [self application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) { - }]; + NSUInteger len = ((token.length > 8) ? 8 : token.length / 2); + NSLog(@"[AppDelegate] Got Push token! (%@ ...)", [token subdataWithRange:NSMakeRange(0, len)]); + + MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; + [accountManager setPushDeviceToken:token withPushOptions:@{@"format": @"event_id_only"}]; + + isPushRegistered = YES; + + if (self.registrationForRemoteNotificationsCompletion) + { + self.registrationForRemoteNotificationsCompletion(nil); + self.registrationForRemoteNotificationsCompletion = nil; + } +} + +- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type +{ + MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; + [accountManager setPushDeviceToken:nil withPushOptions:nil]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type +{ + // Handle the local notifications by triggering a background sync. + [self handleLocalNotifications]; +} + +- (void)handleLocalNotifications +{ + _hasPendingLocalNotifications = NO; + + // Check whether the application is running in background. + if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) + return; + + // Launch a background sync for all existing matrix sessions + NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; + for (MXKAccount *account in mxAccounts) + { + // Check the current session state + if (account.mxSession.state != MXSessionStatePaused) + { + NSLog(@"[AppDelegate] handleLocalNotifications: delay the background sync"); + // Turn on the flag used to trigger a new background sync when a session is paused. + _hasPendingLocalNotifications = YES; + } + + [account backgroundSync:20000 success:^{ + + NSLog(@"[AppDelegate] handleLocalNotifications: the background sync succeeds"); + + // Update icon badge number + [UIApplication sharedApplication].applicationIconBadgeNumber = [account.mxSession riot_missedDiscussionsCount]; + + } failure:^(NSError *error) { + + NSLog(@"[AppDelegate] handleLocalNotifications: the background sync fails"); + + }]; + } +} + +- (nullable NSString *)notificationBodyForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState inAccount:(MXKAccount*)account +{ + if (!event.content || !event.content.count) + return nil; + + NSString *notificationBody; + NSString *eventSenderName = [roomState memberName:event.sender]; + + if (event.eventType == MXEventTypeRoomMessage || event.eventType == MXEventTypeRoomEncrypted) + { + BOOL isDirect = [account.mxSession roomWithRoomId:event.roomId].isDirect; + + NSString *msgType = event.content[@"msgtype"]; + NSString *content = event.content[@"body"]; + + if (event.isEncrypted && !account.showDecryptedContentInNotifications) + { + // Hide the content + msgType = nil; + } + + if (!isDirect) + { + NSString *roomDisplayName = roomState.displayname; + + if ([msgType isEqualToString:@"m.text"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_IN_ROOM_WITH_CONTENT", nil), eventSenderName,roomDisplayName, content]; + else if ([msgType isEqualToString:@"m.emote"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"ACTION_FROM_USER_IN_ROOM", nil), roomDisplayName, eventSenderName, content]; + else if ([msgType isEqualToString:@"m.image"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"IMAGE_FROM_USER_IN_ROOM", nil), eventSenderName, content, roomDisplayName]; + else + // Encrypted messages falls here + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_IN_ROOM", nil), eventSenderName, roomDisplayName]; + } + else + { + if ([msgType isEqualToString:@"m.text"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER_WITH_CONTENT", nil), eventSenderName, content]; + else if ([msgType isEqualToString:@"m.emote"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"ACTION_FROM_USER", nil), eventSenderName, content]; + else if ([msgType isEqualToString:@"m.image"]) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"IMAGE_FROM_USER", nil), eventSenderName, content]; + else + // Encrypted messages falls here + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"MSG_FROM_USER", nil), eventSenderName]; + } + } + else if (event.eventType == MXEventTypeCallInvite) + { + NSString *sdp = event.content[@"offer"][@"sdp"]; + BOOL isVideoCall = [sdp rangeOfString:@"m=video"].location != NSNotFound; + + if (!isVideoCall) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"VOICE_CALL_FROM_USER", nil), eventSenderName]; + else + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"VIDEO_CALL_FROM_USER", nil), eventSenderName]; + } + else if (event.eventType == MXEventTypeRoomMember) + { + NSString *roomName = roomState.name; + NSString *roomAlias = roomState.aliases.firstObject; + + if (roomName) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_NAMED_ROOM", nil), eventSenderName, roomName]; + else if (roomAlias) + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_NAMED_ROOM", nil), eventSenderName, roomAlias]; + else + notificationBody = [NSString stringWithFormat:NSLocalizedString(@"USER_INVITE_TO_CHAT", nil), eventSenderName]; + } + + return notificationBody; } - (void)refreshApplicationIconBadgeNumber @@ -1507,6 +1614,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Get modular widget events in rooms histories [[MXKAppSettings standardAppSettings] addSupportedEventTypes:@[kWidgetEventTypeString]]; + // Use shared container to share data with app extensions + sdkOptions.applicationGroupIdentifier = @"group.im.vector"; + // Disable long press on event in bubble cells [MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:YES]; @@ -1517,13 +1627,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN matrixSessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXSession *mxSession = (MXSession*)notif.object; - // Remove by default potential call observer on matrix session state change - if (matrixCallObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:matrixCallObserver]; - matrixCallObserver = nil; - } - // Check whether the concerned session is a new one if (mxSession.state == MXSessionStateInitialised) { @@ -1545,6 +1648,19 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN if (callStack) { [mxSession enableVoIPWithCallStack:callStack]; + + // Setup CallKit + if ([MXCallKitAdapter callKitAvailable]) + { + BOOL isCallKitEnabled = [MXKAppSettings standardAppSettings].isCallKitEnabled; + [self enableCallKit:isCallKitEnabled forCallManager:mxSession.callManager]; + + // Register for changes performed by the user + [[MXKAppSettings standardAppSettings] addObserver:self + forKeyPath:@"enableCallKit" + options:NSKeyValueObservingOptionNew + context:NULL]; + } } else { @@ -1562,12 +1678,19 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } else if (mxSession.state == MXSessionStateStoreDataReady) { - // Check whether the app user wants inApp notifications on new events for this session + // A new call observer may be added here + [self addMatrixCallObserver]; + + // Enable local notifications + [self enableLocalNotificationsFromMatrixSession:mxSession]; + + // Look for the account related to this session. NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; for (MXKAccount *account in mxAccounts) { if (account.mxSession == mxSession) { + // Enable inApp notifications (if they are allowed for this account). [self enableInAppNotificationsForAccount:account]; break; } @@ -1577,23 +1700,30 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [self removeMatrixSession:mxSession]; } - - // Restore call observer only if all session are running - NSArray *mxSessions = self.mxSessions; - BOOL shouldAddMatrixCallObserver = (mxSessions.count); - for (mxSession in mxSessions) + // Consider here the case where the app is running in background. + else if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) { - if (mxSession.state != MXSessionStateRunning) + if (mxSession.state == MXSessionStateRunning) { - shouldAddMatrixCallObserver = NO; - break; + // Pause the session in background task + NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; + for (MXKAccount *account in mxAccounts) + { + if (account.mxSession == mxSession) + { + [account pauseInBackgroundTask]; + break; + } + } + } + else if (mxSession.state == MXSessionStatePaused) + { + // Check whether some local notifications must be handled by triggering a background sync. + if (_hasPendingLocalNotifications) + { + [self handleLocalNotifications]; + } } - } - - if (shouldAddMatrixCallObserver) - { - // A new call observer may be added here - [self addMatrixCallObserver]; } [self handleLaunchAnimation]; @@ -1614,10 +1744,10 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Set the push gateway URL. account.pushGatewayURL = [[NSUserDefaults standardUserDefaults] objectForKey:@"pushGatewayURL"]; - if (isAPNSRegistered) + if (isPushRegistered) { // Enable push notifications by default on new added account - account.enablePushNotifications = YES; + account.enablePushKitNotifications = YES; } else { @@ -1678,38 +1808,28 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Add observer on settings changes. [[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllEventsInRoomHistory" options:0 context:nil]; - // Observers have been defined, we can start a matrix session for each enabled accounts. - // except if the app is still in background. - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) - { - [self prepareSessionForActiveAccounts]; - } - else - { - // The app is launched in background as a result of a remote notification. - // Presently we are not able to initialize the matrix session(s) in background. (FIXME: initialize matrix session(s) in case of a background launch). - // Patch: the account session(s) will be opened when the app will enter foreground. - NSLog(@"[AppDelegate] initMatrixSessions: The application has been launched in background"); - } -} - -- (void)prepareSessionForActiveAccounts -{ // Prepare account manager MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; // Use MXFileStore as MXStore to permanently store events. accountManager.storeClass = [MXFileStore class]; - // Check the first active account to know whether we have to prepare a matrix session for each account. - NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; - MXKAccount *firstActiveAccount = mxAccounts.firstObject; - if (firstActiveAccount && !firstActiveAccount.mxSession) + // Disable APNS use. + if (accountManager.apnsDeviceToken) + { + // We use now Pushkit, unregister for all remote notifications received via Apple Push Notification service. + [[UIApplication sharedApplication] unregisterForRemoteNotifications]; + [accountManager setApnsDeviceToken:nil]; + } + + // Observers have been defined, we can start a matrix session for each enabled accounts. + NSLog(@"[AppDelegate] initMatrixSessions: prepareSessionForActiveAccounts (app state: %tu)", [[UIApplication sharedApplication] applicationState]); + [accountManager prepareSessionForActiveAccounts]; + + // Check whether we're already logged in + NSArray *mxAccounts = accountManager.activeAccounts; + if (mxAccounts.count) { - NSLog(@"[AppDelegate] prepareSessionForActiveAccounts"); - - [accountManager prepareSessionForActiveAccounts]; - for (MXKAccount *account in mxAccounts) { // Replace default room summary updater @@ -1774,7 +1894,16 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // If any, disable the no VoIP support workaround [self disableNoVoIPOnMatrixSession:mxSession]; + // Disable local notifications from this session + [self disableLocalNotificationsFromMatrixSession:mxSession]; + [mxSessionArray removeObject:mxSession]; + + if (!mxSessionArray.count && matrixCallObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:matrixCallObserver]; + matrixCallObserver = nil; + } } - (void)markAllMessagesAsRead @@ -1811,8 +1940,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)logout { - [[UIApplication sharedApplication] unregisterForRemoteNotifications]; - isAPNSRegistered = NO; + self.pushRegistry = nil; + isPushRegistered = NO; // Clear cache [MXMediaManager clearCache]; @@ -1851,28 +1980,141 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [self enableInAppNotificationsForAccount:(MXKAccount*)object]; } + else if (object == [MXKAppSettings standardAppSettings] && [keyPath isEqualToString:@"enableCallKit"]) + { + BOOL isCallKitEnabled = [MXKAppSettings standardAppSettings].isCallKitEnabled; + MXCallManager *callManager = [[[[[MXKAccountManager sharedManager] activeAccounts] firstObject] mxSession] callManager]; + [self enableCallKit:isCallKitEnabled forCallManager:callManager]; + } } - (void)addMatrixCallObserver { if (matrixCallObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:matrixCallObserver]; + return; } - // Register call observer in order to handle new opened session - matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - + // Register call observer in order to handle incoming calls + matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notif) + { // Ignore the call if a call is already in progress if (!currentCallViewController && !_jitsiViewController) { MXCall *mxCall = (MXCall*)notif.object; - // Prepare the call view controller - currentCallViewController = [CallViewController callViewController:mxCall]; - currentCallViewController.delegate = self; + BOOL isCallKitAvailable = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled; - [self presentCallViewController:nil]; + // Prepare the call view controller + currentCallViewController = [CallViewController callViewController:nil]; + currentCallViewController.playRingtone = !isCallKitAvailable; + currentCallViewController.mxCall = mxCall; + currentCallViewController.delegate = self; + + UIApplicationState applicationState = UIApplication.sharedApplication.applicationState; + + // App has been woken by PushKit notification in the background + if (applicationState == UIApplicationStateBackground && mxCall.isIncoming) + { + // Create backgound task. + // Without CallKit this will allow us to play vibro until the call was ended + // With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources + id handler = [MXSDKOptions sharedInstance].backgroundModeHandler; + NSUInteger callTaskIdentifier = [handler startBackgroundTaskWithName:nil completion:^{}]; + + // Start listening for call state change notifications + __weak NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + __block id token = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange + object:mxCall + queue:nil + usingBlock:^(NSNotification * _Nonnull note) { + MXCall *call = (MXCall *)note.object; + + if (call.state == MXCallStateEnded) + { + // Set call vc to nil to let our app handle new incoming calls even it wasn't killed by the system + currentCallViewController = nil; + [notificationCenter removeObserver:token]; + + [handler endBackgrounTaskWithIdentifier:callTaskIdentifier]; + } + }]; + } + + if (mxCall.isIncoming && !isCallKitAvailable) + { + // Prompt user before presenting the call view controller + NSString *callPromptFormat = mxCall.isVideoCall ? NSLocalizedStringFromTable(@"call_incoming_video_prompt", @"Vector", nil) : NSLocalizedStringFromTable(@"call_incoming_voice_prompt", @"Vector", nil); + NSString *callerName = currentCallViewController.peer.displayname; + if (!callerName.length) + { + callerName = currentCallViewController.peer.userId; + } + NSString *callPrompt = [NSString stringWithFormat:callPromptFormat, callerName]; + + // Removing existing notification (if any) + [_incomingCallNotification dismissViewControllerAnimated:NO completion:nil]; + + _incomingCallNotification = [UIAlertController alertControllerWithTitle:callPrompt + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + __weak typeof(self) weakSelf = self; + + [_incomingCallNotification addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"decline", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + // Reject the call. + // Note: Do not reset the incoming call notification before this operation, because it is used to release properly the dismissed call view controller. + if (self->currentCallViewController) + { + [self->currentCallViewController onButtonPressed:self->currentCallViewController.rejectCallButton]; + + currentCallViewController = nil; + } + + self.incomingCallNotification = nil; + + mxCall.delegate = nil; + } + + }]]; + + [_incomingCallNotification addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"accept", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + self.incomingCallNotification = nil; + + if (self->currentCallViewController) + { + [self->currentCallViewController onButtonPressed:self->currentCallViewController.answerCallButton]; + + [self presentCallViewController:nil]; + } + } + + }]]; + + [_incomingCallNotification mxk_setAccessibilityIdentifier:@"AppDelegateIncomingCallAlert"]; + [self showNotificationAlert:_incomingCallNotification]; + } + else + { + [self presentCallViewController:nil]; + } } }]; } @@ -1979,6 +2221,99 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } +- (void)enableCallKit:(BOOL)enable forCallManager:(MXCallManager *)callManager +{ + if (enable) + { + // Create adapter with default configuration for a while + MXCallKitAdapter *callKitAdapter = [[MXCallKitAdapter alloc] init]; + + id audioSessionConfigurator; + +#ifdef MX_CALL_STACK_JINGLE + audioSessionConfigurator = [[MXJingleCallAudioSessionConfigurator alloc] init]; +#endif + + callKitAdapter.audioSessionConfigurator = audioSessionConfigurator; + + callManager.callKitAdapter = callKitAdapter; + } + else + { + callManager.callKitAdapter = nil; + } +} + +- (void)enableLocalNotificationsFromMatrixSession:(MXSession*)mxSession +{ + __weak typeof(self) weakSelf = self; + + // Look for the account related to this session. + NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; + MXKAccount *account; + for (account in mxAccounts) + { + if (account.mxSession == mxSession) + { + break; + } + account = nil; + } + + MXOnNotification notificationListenerBlock = ^(MXEvent *event, MXRoomState *roomState, MXPushRule *rule) { + + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) + { + // Do not display local notification if the app is not running in background. + return; + } + + // For all type of event show local notifications besides the situation + // when the type of event is call invite and we have CallKit support + BOOL isCallKitActive = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled; + if (!(event.eventType == MXEventTypeCallInvite && isCallKitActive)) + { + NSString *notificationBody = [weakSelf notificationBodyForEvent:event withRoomState:roomState inAccount:account]; + if (notificationBody) + { + UILocalNotification *eventNotification = [[UILocalNotification alloc] init]; + eventNotification.fireDate = [NSDate date]; + eventNotification.alertBody = notificationBody; + eventNotification.userInfo = @{ @"room_id" : event.roomId }; + + // Set sound name based on the value provided in action of MXPushRule + for (MXPushRuleAction *action in rule.actions) + { + if (action.actionType == MXPushRuleActionTypeSetTweak) + { + if ([action.parameters[@"set_tweak"] isEqualToString:@"sound"]) + { + NSString *soundName = action.parameters[@"value"]; + if ([soundName isEqualToString:@"default"]) + soundName = UILocalNotificationDefaultSoundName; + + eventNotification.soundName = soundName; + } + } + } + + [[UIApplication sharedApplication] scheduleLocalNotification:eventNotification]; + } + } + }; + + [mxSession.notificationCenter listenToNotifications:notificationListenerBlock]; + notificationListenerBlocks[@(mxSession.hash)] = notificationListenerBlock; +} + +- (void)disableLocalNotificationsFromMatrixSession:(MXSession*)mxSession +{ + // Stop listening to notification of this session + [mxSession.notificationCenter removeListener:notificationListenerBlocks[@(mxSession.hash)]]; + [notificationListenerBlocks removeObjectForKey:@(mxSession.hash)]; +} + + #pragma mark - /** diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 208c1f75b..6706da321 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -297,6 +297,7 @@ "settings_user_settings" = "USER SETTINGS"; "settings_notifications_settings" = "NOTIFICATION SETTINGS"; +"settings_calls_settings" = "CALLS"; "settings_user_interface" = "USER INTERFACE"; "settings_ignored_users" = "IGNORED USERS"; "settings_contacts" = "LOCAL CONTACTS"; @@ -326,6 +327,7 @@ "settings_fail_to_update_profile" = "Fail to update profile"; "settings_enable_push_notif" = "Notifications on this device"; +"settings_show_decrypted_content" = "Show decrypted content"; "settings_global_settings_info" = "Global notification settings are available on your %@ web client"; "settings_pin_rooms_with_missed_notif" = "Pin rooms with missed notifications"; "settings_pin_rooms_with_unread" = "Pin rooms with unread messages"; @@ -338,6 +340,8 @@ //"settings_join_leave_rooms" = "When people join or leave rooms"; //"settings_call_invitations" = "Call invitations"; +"settings_enable_callkit" = "Integrated calling"; +"settings_callkit_info" = "Receive incoming calls on your lock screen. See your Riot calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; "settings_ui_language" = "Language"; "settings_ui_theme" = "Theme"; "settings_ui_theme_auto" = "Auto"; diff --git a/Riot/Categories/MXSession+Riot.h b/Riot/Categories/MXSession+Riot.h new file mode 100644 index 000000000..9bc296a98 --- /dev/null +++ b/Riot/Categories/MXSession+Riot.h @@ -0,0 +1,28 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import + +@interface MXSession (Riot) + +/** + The current number of rooms with missed notifications, including the invites. + */ +- (NSUInteger)riot_missedDiscussionsCount; + +@end diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m new file mode 100644 index 000000000..3d8805b5e --- /dev/null +++ b/Riot/Categories/MXSession+Riot.m @@ -0,0 +1,51 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "MXSession+Riot.h" + +#import "MXRoom+Riot.h" + +@implementation MXSession (Riot) + +- (NSUInteger)riot_missedDiscussionsCount +{ + NSUInteger missedDiscussionsCount = 0; + + // Sum all the rooms with missed notifications. + for (MXRoomSummary *roomSummary in self.roomsSummaries) + { + NSUInteger notificationCount = roomSummary.notificationCount; + + // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. + if (roomSummary.room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + notificationCount = roomSummary.highlightCount; + } + + if (notificationCount) + { + missedDiscussionsCount++; + } + } + + // Add the invites count + missedDiscussionsCount += [self invitedRooms].count; + + return missedDiscussionsCount; +} + +@end diff --git a/Riot/Info.plist b/Riot/Info.plist index 290cbdbca..da2d99ef9 100644 --- a/Riot/Info.plist +++ b/Riot/Info.plist @@ -49,6 +49,8 @@ The microphone is used to take videos, make calls. NSPhotoLibraryUsageDescription The photo library is used to send photos and videos. + NSSiriUsageDescription + Siri is used to perform calls even from the lock screen. UIBackgroundModes audio diff --git a/Riot/Riot-Defaults.plist b/Riot/Riot-Defaults.plist index af53e3554..34452a887 100644 --- a/Riot/Riot-Defaults.plist +++ b/Riot/Riot-Defaults.plist @@ -12,6 +12,8 @@ im.vector.app.ios.dev pusherAppIdProd im.vector.app.ios.prod + pushKitAppIdProd + im.vector.app.ios.voip.prod identityserverurl https://vector.im homeserverurl @@ -28,8 +30,6 @@ https://scalar-staging.riot.im/scalar-web/ integrationsRestUrl https://scalar-staging.riot.im/scalar/api - apnsDeviceToken - showAllEventsInRoomHistory showRedactionsInRoomHistory diff --git a/Riot/Riot.entitlements b/Riot/Riot.entitlements index bb2281e16..edee34b65 100644 --- a/Riot/Riot.entitlements +++ b/Riot/Riot.entitlements @@ -11,6 +11,8 @@ applinks:riot.im applinks:www.riot.im + com.apple.developer.siri + com.apple.security.application-groups group.im.vector diff --git a/Riot/ViewController/MasterTabBarController.m b/Riot/ViewController/MasterTabBarController.m index 8c02e2c81..fb5d451d3 100644 --- a/Riot/ViewController/MasterTabBarController.m +++ b/Riot/ViewController/MasterTabBarController.m @@ -23,6 +23,7 @@ #import "AppDelegate.h" #import "MXRoom+Riot.h" +#import "MXSession+Riot.h" @interface MasterTabBarController () { @@ -454,26 +455,7 @@ // Considering all the current sessions. for (MXSession *session in mxSessionArray) { - // Sum all the rooms with missed notifications. - for (MXRoomSummary *roomSummary in session.roomsSummaries) - { - NSUInteger notificationCount = roomSummary.notificationCount; - - // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (roomSummary.room.isMentionsOnly) - { - // Only the highlighted missed messages must be considered here. - notificationCount = roomSummary.highlightCount; - } - - if (notificationCount) - { - roomCount ++; - } - } - - // Add the invites count - roomCount += [session invitedRooms].count; + roomCount += [session riot_missedDiscussionsCount]; } return roomCount; diff --git a/Riot/ViewController/SettingsViewController.m b/Riot/ViewController/SettingsViewController.m index b135a201b..20c615f72 100644 --- a/Riot/ViewController/SettingsViewController.m +++ b/Riot/ViewController/SettingsViewController.m @@ -17,13 +17,14 @@ #import "SettingsViewController.h" -#import "AppDelegate.h" - -#import "AvatarGenerator.h" - -#import +#import #import #import +#import +#import + +#import "AppDelegate.h" +#import "AvatarGenerator.h" #import "MXKEncryptionKeysExportView.h" #import "BugReportViewController.h" @@ -32,17 +33,14 @@ #import "CountryPickerViewController.h" #import "LanguagePickerViewController.h" -#import "TableViewCellWithPhoneNumberTextField.h" #import "NBPhoneNumberUtil.h" - -#import "AvatarGenerator.h" - -#import "OLMKit/OLMKit.h" +#import "RageShakeManager.h" +#import "RiotDesignValues.h" +#import "TableViewCellWithPhoneNumberTextField.h" #import "GBDeviceInfo_iOS.h" - NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; enum @@ -50,6 +48,7 @@ enum SETTINGS_SECTION_SIGN_OUT_INDEX = 0, SETTINGS_SECTION_USER_SETTINGS_INDEX, SETTINGS_SECTION_NOTIFICATIONS_SETTINGS_INDEX, + SETTINGS_SECTION_CALLS_INDEX, SETTINGS_SECTION_USER_INTERFACE_INDEX, SETTINGS_SECTION_IGNORED_USERS_INDEX, SETTINGS_SECTION_CONTACTS_INDEX, @@ -64,6 +63,7 @@ enum enum { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, + NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT, NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX, NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX, NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX, @@ -76,6 +76,13 @@ enum NOTIFICATION_SETTINGS_COUNT }; +enum +{ + CALLS_ENABLE_CALLKIT_INDEX = 0, + CALLS_DESCRIPTION_INDEX, + CALLS_COUNT +}; + enum { USER_INTERFACE_LANGUAGE_INDEX = 0, @@ -127,7 +134,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); // listener id removedAccountObserver; id accountUserInfoObserver; - id apnsInfoUpdateObserver; + id pushInfoUpdateObserver; id notificationCenterWillUpdateObserver; id notificationCenterDidUpdateObserver; @@ -273,8 +280,8 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); }]; - // Add observer to apns - apnsInfoUpdateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountAPNSActivityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + // Add observer to push settings + pushInfoUpdateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountPushKitActivityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { [self stopActivityIndicator]; @@ -523,10 +530,10 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); accountUserInfoObserver = nil; } - if (apnsInfoUpdateObserver) + if (pushInfoUpdateObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:apnsInfoUpdateObserver]; - apnsInfoUpdateObserver = nil; + [[NSNotificationCenter defaultCenter] removeObserver:pushInfoUpdateObserver]; + pushInfoUpdateObserver = nil; } [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -1175,6 +1182,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); { count = NOTIFICATION_SETTINGS_COUNT; } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if ([MXCallKitAdapter callKitAvailable]) + { + count = CALLS_COUNT; + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { count = USER_INTERFACE_COUNT; @@ -1616,13 +1630,25 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_enable_push_notif", @"Vector", nil); - labelAndSwitchCell.mxkSwitch.on = account.pushNotificationServiceIsActive; + labelAndSwitchCell.mxkSwitch.on = account.isPushKitNotificationActive; labelAndSwitchCell.mxkSwitch.enabled = YES; [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(togglePushNotifications:) forControlEvents:UIControlEventTouchUpInside]; cell = labelAndSwitchCell; } + else if (row == NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_decrypted_content", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = account.showDecryptedContentInNotifications; + labelAndSwitchCell.mxkSwitch.enabled = account.isPushKitNotificationActive; + [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleShowDecodedContent:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } else if (row == NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX) { MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView]; @@ -1661,6 +1687,29 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); cell = labelAndSwitchCell; } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (row == CALLS_ENABLE_CALLKIT_INDEX) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_enable_callkit", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = [MXKAppSettings standardAppSettings].isCallKitEnabled; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleCallKit:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + else if (row == CALLS_DESCRIPTION_INDEX) + { + MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView]; + globalInfoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_callkit_info", @"Vector", nil); + globalInfoCell.textLabel.numberOfLines = 0; + globalInfoCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = globalInfoCell; + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { if (row == USER_INTERFACE_LANGUAGE_INDEX) @@ -2085,6 +2134,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); { return NSLocalizedStringFromTable(@"settings_notifications_settings", @"Vector", nil); } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if ([MXCallKitAdapter callKitAvailable]) + { + return NSLocalizedStringFromTable(@"settings_calls_settings", @"Vector", nil); + } + } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { return NSLocalizedStringFromTable(@"settings_user_interface", @"Vector", nil); @@ -2209,6 +2265,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); } } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (![MXCallKitAdapter callKitAvailable]) + { + return SECTION_TITLE_PADDING_WHEN_HIDDEN; + } + } return 24; } @@ -2227,6 +2290,13 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); } } } + else if (section == SETTINGS_SECTION_CALLS_INDEX) + { + if (![MXCallKitAdapter callKitAvailable]) + { + return SECTION_TITLE_PADDING_WHEN_HIDDEN; + } + } return 24; } @@ -2656,9 +2726,9 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; MXKAccount* account = accountManager.activeAccounts.firstObject; - if (accountManager.apnsDeviceToken) + if (accountManager.pushDeviceToken) { - [account setEnablePushNotifications:!account.pushNotificationServiceIsActive]; + [account setEnablePushKitNotifications:!account.isPushKitNotificationActive]; } else { @@ -2671,13 +2741,25 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); } else { - [account setEnablePushNotifications:YES]; + [account setEnablePushKitNotifications:YES]; } }]; } } } +- (void)toggleCallKit:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + [MXKAppSettings standardAppSettings].enableCallKit = switchButton.isOn; +} + +- (void)toggleShowDecodedContent:(id)sender +{ + MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + account.showDecryptedContentInNotifications = !account.showDecryptedContentInNotifications; +} + - (void)toggleLocalContactsSync:(id)sender { UISwitch *switchButton = (UISwitch*)sender; diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist new file mode 100644 index 000000000..9fa5f075e --- /dev/null +++ b/SiriIntents/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SiriIntents + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INStartAudioCallIntent + INStartVideoCallIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + IntentHandler + + + diff --git a/SiriIntents/IntentHandler.h b/SiriIntents/IntentHandler.h new file mode 100644 index 000000000..66da341a6 --- /dev/null +++ b/SiriIntents/IntentHandler.h @@ -0,0 +1,21 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface IntentHandler : INExtension + +@end diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m new file mode 100644 index 000000000..5031ac70a --- /dev/null +++ b/SiriIntents/IntentHandler.m @@ -0,0 +1,302 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "IntentHandler.h" + +#import "MXKAccount.h" +#import "MXKAccountManager.h" +#import "MXFileStore.h" +#import "MXSession.h" + +@interface IntentHandler () + +@end + +@implementation IntentHandler + +- (instancetype)init +{ + self = [super init]; + if (self) + { + [MXSDKOptions sharedInstance].applicationGroupIdentifier = @"group.im.vector"; + } + return self; +} + +- (id)handlerForIntent:(INIntent *)intent +{ + return self; +} + +#pragma mark - INStartAudioCallIntentHandling + +- (void)resolveContactsForStartAudioCall:(INStartAudioCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + [self resolveContacts:intent.contacts withCompletion:completion]; +} + +- (void)confirmStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion +{ + INStartAudioCallIntentResponse *response = nil; + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { +#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined MX_CALL_STACK_JINGLE + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)]; + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeReady userActivity:userActivity]; +#else + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil]; +#endif + } + else + { + // User hasn't logged in + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureAppConfigurationRequired userActivity:nil]; + } + + completion(response); +} + +- (void)handleStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion +{ + INStartAudioCallIntentResponse *response = nil; + + INPerson *person = intent.contacts.firstObject; + if (person && person.customIdentifier) + { + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)]; + userActivity.userInfo = @{ @"roomID" : person.customIdentifier }; + + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeContinueInApp + userActivity:userActivity]; + } + else + { + response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailure userActivity:nil]; + } + + completion(response); +} + +#pragma mark - INStartVideoCallIntentHandling + +- (void)resolveContactsForStartVideoCall:(INStartVideoCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + [self resolveContacts:intent.contacts withCompletion:completion]; +} + +- (void)confirmStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion +{ + INStartVideoCallIntentResponse *response = nil; + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { +#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined MX_CALL_STACK_JINGLE + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)]; + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeReady userActivity:userActivity]; +#else + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil]; +#endif + } + else + { + // User hasn't logged in + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureRequiringAppLaunch userActivity:nil]; + } + + completion(response); +} + +- (void)handleStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion +{ + INStartVideoCallIntentResponse *response = nil; + + INPerson *person = intent.contacts.firstObject; + if (person && person.customIdentifier) + { + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)]; + userActivity.userInfo = @{ @"roomID" : person.customIdentifier }; + + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeContinueInApp + userActivity:userActivity]; + } + else + { + response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailure userActivity:nil]; + } + + completion(response); +} + +#pragma mark - Private + +- (void)resolveContacts:(nullable NSArray *)contacts withCompletion:(void (^)(NSArray * _Nonnull))completion +{ + if (contacts.count == 0) + { + completion(@[[INPersonResolutionResult needsValue]]); + return; + } + else + { + // We don't iterate over array of contacts from passed intent + // since it's hard to imagine scenario with several callee + // so we just extract the first one + INPerson *callee = contacts.firstObject; + + // If this method is called after selection of the appropriate user, it will hold userId of an user to whom we must call + NSString *selectedUserId; + + // Check if the user has selected right room among several direct rooms from previous resolution process run + if (callee.customIdentifier.length) + { + // If callee will have the same name as one of the contact in the system contacts app + // Siri will pass us this contact in the intent.contacts array and we must provide the same count of + // resolution results as elements count in the intent.contact. + // So we just pass the same result at all iterations + NSMutableArray *resolutionResults = [NSMutableArray array]; + for (NSInteger i = 0; i < contacts.count; ++i) + [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:callee]]; + completion(resolutionResults); + return; + } + else + { + // This resolution process run after selecting appropriate user among suggested user list + selectedUserId = callee.personHandle.value; + } + + MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + if (account) + { + MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials]; + [fileStore asyncRoomsSummaries:^(NSArray * _Nonnull roomsSummaries) { + + // Contains userIds of all users with whom the current user has direct chats + // Use set to avoid duplicates + NSMutableSet *directUserIds = [NSMutableSet set]; + + // Contains room summaries for all direct rooms connected with particular userId + NSMutableDictionary *> *roomSummaries = [NSMutableDictionary dictionary]; + + for (MXRoomSummary *summary in roomsSummaries) + { + // TODO: We also need to check if joined room members count equals 2 + // It is pointlessly to save rooms with 1 joined member or room with more than 2 joined members + if (summary.isDirect) + { + NSString *diretUserId = summary.directUserId; + + // Collect room summaries only for specified user + if (selectedUserId && ![diretUserId isEqualToString:selectedUserId]) + continue; + + // Save userId + [directUserIds addObject:diretUserId]; + + // Save associated with diretUserId room summary + NSMutableArray *userRoomSummaries = roomSummaries[diretUserId]; + if (userRoomSummaries) + [userRoomSummaries addObject:summary]; + else + roomSummaries[diretUserId] = [NSMutableArray arrayWithObject:summary]; + } + } + + [fileStore asyncUsersWithUserIds:directUserIds.allObjects success:^(NSArray * _Nonnull users) { + + // Find users whose display name contains string presented us by Siri + NSMutableArray *matchingUsers = [NSMutableArray array]; + for (MXUser *user in users) + { + if (!user.displayname) + continue; + + if (!NSEqualRanges([callee.displayName rangeOfString:user.displayname options:NSCaseInsensitiveSearch], (NSRange){NSNotFound,0})) + { + [matchingUsers addObject:user]; + } + } + + NSMutableArray *persons = [NSMutableArray array]; + + if (matchingUsers.count == 1) + { + MXUser *user = matchingUsers.firstObject; + + // Provide to the user a list of direct rooms to choose from + NSArray *summaries = roomSummaries[user.userId]; + for (MXRoomSummary *summary in summaries) + { + INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; + + // For rooms we try to use room display name + NSString *displayName = summary.displayname ? summary.displayname : user.displayname; + + INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle + nameComponents:nil + displayName:displayName + image:nil + contactIdentifier:nil + customIdentifier:summary.roomId]; + + [persons addObject:person]; + } + } + else if (matchingUsers.count > 1) + { + // Provide to the user a list of users to choose from + // This is the case when there are several users with the same name + for (MXUser *user in matchingUsers) + { + INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; + INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle + nameComponents:nil + displayName:user.displayname + image:nil + contactIdentifier:nil + customIdentifier:nil]; + + [persons addObject:person]; + } + } + + if (persons.count == 0) + { + completion(@[[INPersonResolutionResult unsupported]]); + } + else if (persons.count == 1) + { + completion(@[[INPersonResolutionResult successWithResolvedPerson:persons.firstObject]]); + } + else + { + completion(@[[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:persons]]); + } + } failure:nil]; + } failure:nil]; + } + else + { + completion(@[[INPersonResolutionResult notRequired]]); + } + } +} + +@end diff --git a/SiriIntents/SiriIntents.entitlements b/SiriIntents/SiriIntents.entitlements new file mode 100644 index 000000000..e540aaaec --- /dev/null +++ b/SiriIntents/SiriIntents.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.im.vector + + + diff --git a/use-dev-pods.sh b/use-dev-pods.sh index dd3d75323..7262b294c 100755 --- a/use-dev-pods.sh +++ b/use-dev-pods.sh @@ -12,5 +12,5 @@ rm -f Podfile.lock sed -i '' -E "s!^(pod)(.*MatrixSDK)!#\1\2!g" Podfile sed -i '' -E "s!^(pod)(.*MatrixKit)!#\1\2!g" Podfile # And enable the develop ones -sed -i '' -E "s!^(#pod)(.*MatrixSDK)(.*develop)!pod\2\3!g" Podfile -sed -i '' -E "s!^(#pod)(.*MatrixKit)(.*develop)!pod\2\3!g" Podfile +sed -i '' -E "s!^(#pod)(.*MatrixSDK)(.*callkit)!pod\2\3!g" Podfile +sed -i '' -E "s!^(#pod)(.*MatrixKit)(.*callkit)!pod\2\3!g" Podfile