From 889644f7bd28c7c24fc48bc98d3b30b452281667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Thu, 18 Oct 2018 15:54:26 +0200 Subject: [PATCH] [iOS] Add support for Siri shortcuts This is mostly implemented in the app, with the needed support in the SDK. Since the app needs to donate intents and deal with creating NSUserActivity objects it doesn't feel right to do this in a library. Instead, we donate the intents from the app, but the SDK is ready to extract conference URLs from any intent which was registered as a conference activity. This also opens the door for eventually adding Handoff support. --- ios/app/app.entitlements | 2 ++ ios/app/app.xcodeproj/project.pbxproj | 5 +++ ios/app/src/AppDelegate.m | 5 +++ ios/app/src/Info.plist | 4 +++ ios/app/src/Types.h | 7 ++++ ios/app/src/ViewController.m | 47 +++++++++++++++++++++++++-- ios/sdk/src/JitsiMeetView+Private.h | 1 + ios/sdk/src/JitsiMeetView.h | 2 ++ ios/sdk/src/JitsiMeetView.m | 19 +++++++++++ 9 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 ios/app/src/Types.h diff --git a/ios/app/app.entitlements b/ios/app/app.entitlements index 939bee422..accd78bb6 100644 --- a/ios/app/app.entitlements +++ b/ios/app/app.entitlements @@ -7,5 +7,7 @@ applinks:beta.meet.jit.si applinks:meet.jit.si + com.apple.developer.siri + diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index b73552d21..a2c85bf1d 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 0B412F201EDEE95300B1A0A6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0BBD021F212EB69D00CCB19F /* Types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Types.h; sourceTree = ""; }; 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -96,6 +97,7 @@ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, 0B412F201EDEE95300B1A0A6 /* Main.storyboard */, + 0BBD021F212EB69D00CCB19F /* Types.h */, 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */, 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */, ); @@ -173,6 +175,9 @@ com.apple.SafariKeychain = { enabled = 1; }; + com.apple.Siri = { + enabled = 1; + }; }; }; }; diff --git a/ios/app/src/AppDelegate.m b/ios/app/src/AppDelegate.m index 3768cf44a..3de9b1417 100644 --- a/ios/app/src/AppDelegate.m +++ b/ios/app/src/AppDelegate.m @@ -17,6 +17,7 @@ #import "AppDelegate.h" #import "FIRUtilities.h" +#import "Types.h" #import @@ -37,6 +38,10 @@ [Fabric with:@[[Crashlytics class]]]; } + // Set the conference activity type defined in this application. + // This cannot be defined by the SDK. + JitsiMeetView.conferenceActivityType = JitsiMeetConferenceActivityType; + return [JitsiMeetView application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/ios/app/src/Info.plist b/ios/app/src/Info.plist index 335b12c90..b2a7c1960 100644 --- a/ios/app/src/Info.plist +++ b/ios/app/src/Info.plist @@ -97,5 +97,9 @@ firebase_crashlytics_collection_enabled false + NSUserActivityTypes + + org.jitsi.JitsiMeet.ios.conference + diff --git a/ios/app/src/Types.h b/ios/app/src/Types.h new file mode 100644 index 000000000..c44cd888e --- /dev/null +++ b/ios/app/src/Types.h @@ -0,0 +1,7 @@ + +#import + +// This must match what's defined in the NSUserActivityTypes array in the +// Info.plist file. +static NSString *const JitsiMeetConferenceActivityType + = @"org.jitsi.JitsiMeet.ios.conference"; diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 601ea0be0..205e7e3bf 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -14,8 +14,16 @@ * limitations under the License. */ +#import +#import +#import + +#import "Types.h" #import "ViewController.h" +// Needed for NSUserActivity suggestedInvocationPhrase +@import Intents; + /** * The query to perform through JMAddPeopleController when the InviteButton is * tapped in order to exercise the public API of the feature invite. If nil, the @@ -33,11 +41,10 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [super viewDidLoad]; JitsiMeetView *view = (JitsiMeetView *) self.view; + view.delegate = self; #ifdef DEBUG - view.delegate = self; - // inviteController JMInviteController *inviteController = view.inviteController; inviteController.delegate = self; @@ -56,12 +63,13 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [view loadURL:nil]; } -#if DEBUG + // JitsiMeetViewDelegate - (void)_onJitsiMeetViewDelegateEvent:(NSString *)name withData:(NSDictionary *)data { +#if DEBUG NSLog( @"[%s:%d] JitsiMeetViewDelegate %@ %@", __FILE__, __LINE__, name, data); @@ -70,6 +78,7 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [NSThread isMainThread], @"JitsiMeetViewDelegate %@ method invoked on a non-main thread", name); +#endif } - (void)conferenceFailed:(NSDictionary *)data { @@ -78,6 +87,36 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; - (void)conferenceJoined:(NSDictionary *)data { [self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_JOINED" withData:data]; + + // Register a NSUserActivity for this conference so it can be invoked as a + // Siri shortcut. This is only supported in iOS >= 12. +#ifdef __IPHONE_12_0 + if (@available(iOS 12.0, *)) { + NSUserActivity *userActivity + = [[NSUserActivity alloc] initWithActivityType:JitsiMeetConferenceActivityType]; + + NSString *urlStr = data[@"url"]; + NSURL *url = [NSURL URLWithString:urlStr]; + NSString *conference = [url.pathComponents lastObject]; + + userActivity.title = [NSString stringWithFormat:@"Join %@", conference]; + userActivity.suggestedInvocationPhrase = @"Join my Jitsi meeting"; + userActivity.userInfo = @{@"url": urlStr}; + [userActivity setEligibleForSearch:YES]; + [userActivity setEligibleForPrediction:YES]; + [userActivity setPersistentIdentifier:urlStr]; + + // Subtitle + CSSearchableItemAttributeSet *attributes + = [[CSSearchableItemAttributeSet alloc] initWithItemContentType:(NSString *)kUTTypeItem]; + attributes.contentDescription = urlStr; + userActivity.contentAttributeSet = attributes; + + self.userActivity = userActivity; + [userActivity becomeCurrent]; + } +#endif + } - (void)conferenceLeft:(NSDictionary *)data { @@ -96,6 +135,8 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [self _onJitsiMeetViewDelegateEvent:@"LOAD_CONFIG_ERROR" withData:data]; } +#if DEBUG + // JMInviteControllerDelegate - (void)beginAddPeople:(JMAddPeopleController *)addPeopleController { diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h index cc167c8a0..b62501746 100644 --- a/ios/sdk/src/JitsiMeetView+Private.h +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -18,6 +18,7 @@ @interface JitsiMeetView () ++ (NSDictionary *)conferenceURLFromUserActivity:(NSUserActivity *)userActivity; + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; @end diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index 26c409c5f..f0a0d69ad 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -22,6 +22,8 @@ @interface JitsiMeetView : UIView +@property (class, copy, nonatomic, nullable) NSString *conferenceActivityType; + @property (copy, nonatomic, nullable) NSURL *defaultURL; @property (nonatomic, nullable, weak) id delegate; diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 776fafa41..d13f4e65c 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -88,6 +88,8 @@ void registerFatalErrorHandler() { @dynamic pictureInPictureEnabled; +static NSString *_conferenceActivityType; + static RCTBridgeWrapper *bridgeWrapper; /** @@ -277,6 +279,16 @@ static NSMapTable *views; [self loadURLObject:urlString ? @{ @"url": urlString } : nil]; } +#pragma conferenceActivityType getter / setter + ++ (NSString *)conferenceActivityType { + return _conferenceActivityType; +} + ++ (void) setConferenceActivityType:(NSString *)conferenceActivityType { + _conferenceActivityType = conferenceActivityType; +} + #pragma pictureInPictureEnabled getter / setter - (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled { @@ -358,6 +370,13 @@ static NSMapTable *views; @"url": url }; } + } else if (_conferenceActivityType && [activityType isEqualToString:_conferenceActivityType]) { + // App was started by continuing a registered NSUserActivity (SiriKit, Handoff, ...) + NSString *url; + + if ((url = userActivity.userInfo[@"url"])) { + return @{ @"url" : url }; + } } return nil;