You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

JitsiMeetView.m 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /*
  2. * Copyright @ 2017-present Atlassian Pty Ltd
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <CoreText/CoreText.h>
  17. #import <Intents/Intents.h>
  18. #include <mach/mach_time.h>
  19. #import <React/RCTAssert.h>
  20. #import <React/RCTLinkingManager.h>
  21. #import <React/RCTRootView.h>
  22. #import "Invite+Private.h"
  23. #import "InviteController+Private.h"
  24. #import "JitsiMeetView+Private.h"
  25. #import "RCTBridgeWrapper.h"
  26. /**
  27. * A `RCTFatalHandler` implementation which swallows JavaScript errors. In the
  28. * Release configuration, React Native will (intentionally) raise an unhandled
  29. * `NSException` for an unhandled JavaScript error. This will effectively kill
  30. * the application. `_RCTFatal` is suitable to be in accord with the Web i.e.
  31. * not kill the application.
  32. */
  33. RCTFatalHandler _RCTFatal = ^(NSError *error) {
  34. id jsStackTrace = error.userInfo[RCTJSStackTraceKey];
  35. @try {
  36. NSString *name
  37. = [NSString stringWithFormat:@"%@: %@",
  38. RCTFatalExceptionName,
  39. error.localizedDescription];
  40. NSString *message
  41. = RCTFormatError(error.localizedDescription, jsStackTrace, 75);
  42. [NSException raise:name format:@"%@", message];
  43. } @catch (NSException *e) {
  44. if (!jsStackTrace) {
  45. @throw;
  46. }
  47. }
  48. };
  49. /**
  50. * Helper function to dynamically load custom fonts. The `UIAppFonts` key in the
  51. * plist file doesn't work for frameworks, so fonts have to be manually loaded.
  52. */
  53. void loadCustomFonts(Class clazz) {
  54. NSBundle *bundle = [NSBundle bundleForClass:clazz];
  55. NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"];
  56. for (NSString *item in fonts) {
  57. NSString *fontName = [item stringByDeletingPathExtension];
  58. NSString *fontExt = [item pathExtension];
  59. NSString *fontPath = [bundle pathForResource:fontName ofType:fontExt];
  60. NSData *inData = [NSData dataWithContentsOfFile:fontPath];
  61. CFErrorRef error;
  62. CGDataProviderRef provider
  63. = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
  64. CGFontRef font = CGFontCreateWithDataProvider(provider);
  65. if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
  66. CFStringRef errorDescription = CFErrorCopyDescription(error);
  67. NSLog(@"Failed to load font: %@", errorDescription);
  68. CFRelease(errorDescription);
  69. }
  70. CFRelease(font);
  71. CFRelease(provider);
  72. }
  73. }
  74. /**
  75. * Helper function to register a fatal error handler for React. Our handler
  76. * won't kill the process, it will swallow JS errors and print stack traces
  77. * instead.
  78. */
  79. void registerFatalErrorHandler() {
  80. #if !DEBUG
  81. // In the Release configuration, React Native will (intentionally) raise an
  82. // unhandled `NSException` for an unhandled JavaScript error. This will
  83. // effectively kill the application. In accord with the Web, do not kill the
  84. // application.
  85. if (!RCTGetFatalHandler()) {
  86. RCTSetFatalHandler(_RCTFatal);
  87. }
  88. #endif
  89. }
  90. @interface JitsiMeetView() {
  91. /**
  92. * The unique identifier of this `JitsiMeetView` within the process for the
  93. * purposes of `ExternalAPI`. The name scope was inspired by postis which we
  94. * use on Web for the similar purposes of the iframe-based external API.
  95. */
  96. NSString *externalAPIScope;
  97. RCTRootView *rootView;
  98. }
  99. @end
  100. @implementation JitsiMeetView {
  101. NSNumber *_pictureInPictureEnabled;
  102. }
  103. @dynamic pictureInPictureEnabled;
  104. static RCTBridgeWrapper *bridgeWrapper;
  105. /**
  106. * Copy of the `launchOptions` dictionary that the application was started with.
  107. * It is required for the initial URL to be used if a (Universal) link was used
  108. * to launch a new instance of the application.
  109. */
  110. static NSDictionary *_launchOptions;
  111. /**
  112. * The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique
  113. * identifiers within the process).
  114. */
  115. static NSMapTable<NSString *, JitsiMeetView *> *views;
  116. + (BOOL)application:(UIApplication *)application
  117. didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  118. // Store launch options, will be used when we create the bridge.
  119. _launchOptions = [launchOptions copy];
  120. return YES;
  121. }
  122. #pragma mark Linking delegate helpers
  123. // https://facebook.github.io/react-native/docs/linking.html
  124. + (BOOL)application:(UIApplication *)application
  125. continueUserActivity:(NSUserActivity *)userActivity
  126. restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
  127. {
  128. NSString *activityType = userActivity.activityType;
  129. // XXX At least twice we received bug reports about malfunctioning loadURL
  130. // in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
  131. // expected in our testing. But that was to be expected because the app does
  132. // not exercise loadURL. In order to increase the test coverage of loadURL,
  133. // channel Universal linking through loadURL.
  134. if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
  135. && [self loadURLInViews:userActivity.webpageURL]) {
  136. return YES;
  137. }
  138. // Check for a CallKit intent.
  139. if ([activityType isEqualToString:@"INStartAudioCallIntent"]
  140. || [activityType isEqualToString:@"INStartVideoCallIntent"]) {
  141. INIntent *intent = userActivity.interaction.intent;
  142. NSArray<INPerson *> *contacts;
  143. NSString *url;
  144. BOOL startAudioOnly = NO;
  145. if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
  146. contacts = ((INStartAudioCallIntent *) intent).contacts;
  147. startAudioOnly = YES;
  148. } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
  149. contacts = ((INStartVideoCallIntent *) intent).contacts;
  150. }
  151. if (contacts && (url = contacts.firstObject.personHandle.value)) {
  152. // Load the URL contained in the handle.
  153. [self loadURLObjectInViews:@{
  154. @"config": @{
  155. @"startAudioOnly": @(startAudioOnly)
  156. },
  157. @"url": url
  158. }];
  159. return YES;
  160. }
  161. }
  162. return [RCTLinkingManager application:application
  163. continueUserActivity:userActivity
  164. restorationHandler:restorationHandler];
  165. }
  166. + (BOOL)application:(UIApplication *)application
  167. openURL:(NSURL *)url
  168. sourceApplication:(NSString *)sourceApplication
  169. annotation:(id)annotation {
  170. // XXX At least twice we received bug reports about malfunctioning loadURL
  171. // in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
  172. // expected in our testing. But that was to be expected because the app does
  173. // not exercise loadURL. In order to increase the test coverage of loadURL,
  174. // channel Universal linking through loadURL.
  175. if ([self loadURLInViews:url]) {
  176. return YES;
  177. }
  178. return [RCTLinkingManager application:application
  179. openURL:url
  180. sourceApplication:sourceApplication
  181. annotation:annotation];
  182. }
  183. #pragma mark Initializers
  184. - (instancetype)init {
  185. self = [super init];
  186. if (self) {
  187. [self initWithXXX];
  188. }
  189. return self;
  190. }
  191. - (instancetype)initWithCoder:(NSCoder *)coder {
  192. self = [super initWithCoder:coder];
  193. if (self) {
  194. [self initWithXXX];
  195. }
  196. return self;
  197. }
  198. - (instancetype)initWithFrame:(CGRect)frame {
  199. self = [super initWithFrame:frame];
  200. if (self) {
  201. [self initWithXXX];
  202. }
  203. return self;
  204. }
  205. #pragma mark API
  206. /**
  207. * Loads a specific `NSURL` which may identify a conference to join. If the
  208. * specified `NSURL` is `nil` and the Welcome page is enabled, the Welcome page
  209. * is displayed instead.
  210. *
  211. * @param url The `NSURL` to load which may identify a conference to join.
  212. */
  213. - (void)loadURL:(NSURL *)url {
  214. [self loadURLString:url ? url.absoluteString : nil];
  215. }
  216. /**
  217. * Loads a specific URL which may identify a conference to join. The URL is
  218. * specified in the form of an `NSDictionary` of properties which (1)
  219. * internally are sufficient to construct a URL `NSString` while (2) abstracting
  220. * the specifics of constructing the URL away from API clients/consumers. If the
  221. * specified URL is `nil` and the Welcome page is enabled, the Welcome page is
  222. * displayed instead.
  223. *
  224. * @param urlObject The URL to load which may identify a conference to join.
  225. */
  226. - (void)loadURLObject:(NSDictionary *)urlObject {
  227. NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
  228. if (self.defaultURL) {
  229. props[@"defaultURL"] = [self.defaultURL absoluteString];
  230. }
  231. props[@"externalAPIScope"] = externalAPIScope;
  232. props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
  233. props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
  234. props[@"addPeopleEnabled"] = @(_inviteController.addPeopleEnabled);
  235. props[@"dialOutEnabled"] = @(_inviteController.dialOutEnabled);
  236. // XXX If urlObject is nil, then it must appear as undefined in the
  237. // JavaScript source code so that we check the launchOptions there.
  238. if (urlObject) {
  239. props[@"url"] = urlObject;
  240. }
  241. // XXX The method loadURLObject: is supposed to be imperative i.e. a second
  242. // invocation with one and the same URL is expected to join the respective
  243. // conference again if the first invocation was followed by leaving the
  244. // conference. However, React and, respectively,
  245. // appProperties/initialProperties are declarative expressions i.e. one and
  246. // the same URL will not trigger componentWillReceiveProps in the JavaScript
  247. // source code. The workaround implemented bellow introduces imperativeness
  248. // in React Component props by defining a unique value per loadURLObject:
  249. // invocation.
  250. props[@"timestamp"] = @(mach_absolute_time());
  251. if (rootView) {
  252. // Update props with the new URL.
  253. rootView.appProperties = props;
  254. } else {
  255. rootView
  256. = [[RCTRootView alloc] initWithBridge:bridgeWrapper.bridge
  257. moduleName:@"App"
  258. initialProperties:props];
  259. rootView.backgroundColor = self.backgroundColor;
  260. // Add rootView as a subview which completely covers this one.
  261. [rootView setFrame:[self bounds]];
  262. rootView.autoresizingMask
  263. = UIViewAutoresizingFlexibleWidth
  264. | UIViewAutoresizingFlexibleHeight;
  265. [self addSubview:rootView];
  266. }
  267. }
  268. /**
  269. * Loads a specific URL `NSString` which may identify a conference to
  270. * join. If the specified URL `NSString` is `nil` and the Welcome page is
  271. * enabled, the Welcome page is displayed instead.
  272. *
  273. * @param urlString The URL `NSString` to load which may identify a conference
  274. * to join.
  275. */
  276. - (void)loadURLString:(NSString *)urlString {
  277. [self loadURLObject:urlString ? @{ @"url": urlString } : nil];
  278. }
  279. #pragma pictureInPictureEnabled getter / setter
  280. - (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled {
  281. _pictureInPictureEnabled
  282. = [NSNumber numberWithBool:pictureInPictureEnabled];
  283. }
  284. - (BOOL) pictureInPictureEnabled {
  285. if (_pictureInPictureEnabled) {
  286. return [_pictureInPictureEnabled boolValue];
  287. }
  288. // The SDK/JitsiMeetView client/consumer did not explicitly enable/disable
  289. // Picture-in-Picture. However, we may automatically deduce their
  290. // intentions: we need the support of the client in order to implement
  291. // Picture-in-Picture on iOS (in contrast to Android) so if the client
  292. // appears to have provided the support then we can assume that they did it
  293. // with the intention to have Picture-in-Picture enabled.
  294. return self.delegate
  295. && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)];
  296. }
  297. #pragma mark Private methods
  298. /**
  299. * Loads a specific `NSURL` in all existing `JitsiMeetView`s.
  300. *
  301. * @param url The `NSURL` to load in all existing `JitsiMeetView`s.
  302. * @return `YES` if the specified `url` was submitted for loading in at least
  303. * one `JitsiMeetView`; otherwise, `NO`.
  304. */
  305. + (BOOL)loadURLInViews:(NSURL *)url {
  306. return
  307. [self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil];
  308. }
  309. + (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject {
  310. BOOL handled = NO;
  311. if (views) {
  312. for (NSString *externalAPIScope in views) {
  313. JitsiMeetView *view
  314. = [self viewForExternalAPIScope:externalAPIScope];
  315. if (view) {
  316. [view loadURLObject:urlObject];
  317. handled = YES;
  318. }
  319. }
  320. }
  321. return handled;
  322. }
  323. + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope {
  324. return [views objectForKey:externalAPIScope];
  325. }
  326. /**
  327. * Internal initialization:
  328. *
  329. * - sets the background color
  330. * - creates the React bridge
  331. * - loads the necessary custom fonts
  332. * - registers a custom fatal error error handler for React
  333. */
  334. - (void)initWithXXX {
  335. static dispatch_once_t dispatchOncePredicate;
  336. dispatch_once(&dispatchOncePredicate, ^{
  337. // Initialize the static state of JitsiMeetView.
  338. bridgeWrapper
  339. = [[RCTBridgeWrapper alloc] initWithLaunchOptions:_launchOptions];
  340. views = [NSMapTable strongToWeakObjectsMapTable];
  341. // Dynamically load custom bundled fonts.
  342. loadCustomFonts(self.class);
  343. // Register a fatal error handler for React.
  344. registerFatalErrorHandler();
  345. });
  346. // Hook this JitsiMeetView into ExternalAPI.
  347. externalAPIScope = [NSUUID UUID].UUIDString;
  348. [views setObject:self forKey:externalAPIScope];
  349. Invite *inviteModule = [bridgeWrapper.bridge moduleForName:@"Invite"];
  350. _inviteController
  351. = [[InviteController alloc] initWithExternalAPIScope:externalAPIScope
  352. andInviteModule:inviteModule];
  353. // Set a background color which is in accord with the JavaScript and Android
  354. // parts of the application and causes less perceived visual flicker than
  355. // the default background color.
  356. self.backgroundColor
  357. = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
  358. }
  359. @end