FIRIAMActionURLFollower.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*
  2. * Copyright 2018 Google
  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 <Foundation/Foundation.h>
  17. #import <UIKit/UIKit.h>
  18. #import "FIRCore+InAppMessaging.h"
  19. #import "FIRIAMActionURLFollower.h"
  20. @interface FIRIAMActionURLFollower ()
  21. @property(nonatomic, readonly, nonnull, copy) NSSet<NSString *> *appCustomURLSchemesSet;
  22. @property(nonatomic, readonly) BOOL isOldAppDelegateOpenURLDefined;
  23. @property(nonatomic, readonly) BOOL isNewAppDelegateOpenURLDefined;
  24. @property(nonatomic, readonly) BOOL isContinueUserActivityMethodDefined;
  25. @property(nonatomic, readonly, nullable) id<UIApplicationDelegate> appDelegate;
  26. @property(nonatomic, readonly, nonnull) UIApplication *mainApplication;
  27. @end
  28. @implementation FIRIAMActionURLFollower
  29. + (FIRIAMActionURLFollower *)actionURLFollower {
  30. static FIRIAMActionURLFollower *URLFollower;
  31. static dispatch_once_t onceToken;
  32. dispatch_once(&onceToken, ^{
  33. NSMutableArray<NSString *> *customSchemeURLs = [[NSMutableArray alloc] init];
  34. // Reading the custom url list from the environment.
  35. NSBundle *appBundle = [NSBundle mainBundle];
  36. if (appBundle) {
  37. id URLTypesID = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
  38. if ([URLTypesID isKindOfClass:[NSArray class]]) {
  39. NSArray *urlTypesArray = (NSArray *)URLTypesID;
  40. for (id nextURLType in urlTypesArray) {
  41. if ([nextURLType isKindOfClass:[NSDictionary class]]) {
  42. NSDictionary *nextURLTypeDict = (NSDictionary *)nextURLType;
  43. id nextSchemeArray = nextURLTypeDict[@"CFBundleURLSchemes"];
  44. if (nextSchemeArray && [nextSchemeArray isKindOfClass:[NSArray class]]) {
  45. [customSchemeURLs addObjectsFromArray:nextSchemeArray];
  46. }
  47. }
  48. }
  49. }
  50. }
  51. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM300010",
  52. @"Detected %d custom URL schemes from environment", (int)customSchemeURLs.count);
  53. if ([NSThread isMainThread]) {
  54. // We can not dispatch sychronously to main queue if we are already in main queue. That
  55. // can cause deadlock.
  56. URLFollower = [[FIRIAMActionURLFollower alloc]
  57. initWithCustomURLSchemeArray:customSchemeURLs
  58. withApplication:UIApplication.sharedApplication];
  59. } else {
  60. // If we are not on main thread, dispatch it to main queue since it invovles calling UIKit
  61. // methods, which are required to be carried out on main queue.
  62. dispatch_sync(dispatch_get_main_queue(), ^{
  63. URLFollower = [[FIRIAMActionURLFollower alloc]
  64. initWithCustomURLSchemeArray:customSchemeURLs
  65. withApplication:UIApplication.sharedApplication];
  66. });
  67. }
  68. });
  69. return URLFollower;
  70. }
  71. - (instancetype)initWithCustomURLSchemeArray:(NSArray<NSString *> *)customURLScheme
  72. withApplication:(UIApplication *)application {
  73. if (self = [super init]) {
  74. _appCustomURLSchemesSet = [NSSet setWithArray:customURLScheme];
  75. _mainApplication = application;
  76. _appDelegate = [application delegate];
  77. if (_appDelegate) {
  78. _isOldAppDelegateOpenURLDefined = [_appDelegate
  79. respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)];
  80. _isNewAppDelegateOpenURLDefined =
  81. [_appDelegate respondsToSelector:@selector(application:openURL:options:)];
  82. _isContinueUserActivityMethodDefined = [_appDelegate
  83. respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)];
  84. }
  85. }
  86. return self;
  87. }
  88. - (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion {
  89. // So this is the logic of the url following flow
  90. // 1 If it's a http or https link
  91. // 1.1 If delegate implements application:continueUserActivity:restorationHandler: and calling
  92. // it returns YES: the flow stops here: we have finished the url-following action
  93. // 1.2 In other cases: fall through to step 3
  94. // 2 If the URL scheme matches any element in appCustomURLSchemes
  95. // 2.1 Triggers application:openURL:options: or
  96. // application:openURL:sourceApplication:annotation:
  97. // depending on their availability.
  98. // 3 Use UIApplication openURL: or openURL:options:completionHandler: to have iOS system to deal
  99. // with the url following.
  100. //
  101. // The rationale for doing step 1 and 2 instead of simply doing step 3 for all cases are:
  102. // I) calling UIApplication openURL with the universal link targeted for current app would
  103. // not cause the link being treated as a universal link. See apple doc at
  104. // https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html
  105. // So step 1 is trying to handle this gracefully
  106. // II) If there are other apps on the same device declaring the same custom url scheme as for
  107. // the current app, doing step 3 directly have the risk of triggering another app for
  108. // handling the custom scheme url: See the note about "If more than one third-party" from
  109. // https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html
  110. // So step 2 is to optimize user experience by short-circuiting the engagement with iOS
  111. // system
  112. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"Following action url %@", actionURL);
  113. if ([self.class isHttpOrHttpsScheme:actionURL]) {
  114. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", @"Try to treat it as a universal link.");
  115. if ([self followURLWithContinueUserActivity:actionURL]) {
  116. completion(YES);
  117. return; // following the url has been fully handled by App Delegate's
  118. // continueUserActivity method
  119. }
  120. } else if ([self isCustomSchemeForCurrentApp:actionURL]) {
  121. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", @"Custom URL scheme matches.");
  122. if ([self followURLWithAppDelegateOpenURLActivity:actionURL]) {
  123. completion(YES);
  124. return; // following the url has been fully handled by App Delegate's openURL method
  125. }
  126. }
  127. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", @"Open the url via iOS.");
  128. [self followURLViaIOS:actionURL withCompletionBlock:completion];
  129. }
  130. // Try to handle the url as a custom scheme url link by triggering
  131. // application:openURL:options: on App's delegate object directly.
  132. // @returns YES if that delegate method is defined and returns YES.
  133. - (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url {
  134. if (self.isNewAppDelegateOpenURLDefined) {
  135. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008",
  136. @"iOS 9+ version of App Delegate's application:openURL:options: method detected");
  137. #pragma clang diagnostic push
  138. #pragma clang diagnostic ignored "-Wunguarded-availability"
  139. return [self.appDelegate application:self.mainApplication openURL:url options:@{}];
  140. #pragma clang pop
  141. }
  142. // if we come here, we can try to trigger the older version of openURL method on the app's
  143. // delegate
  144. if (self.isOldAppDelegateOpenURLDefined) {
  145. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240009",
  146. @"iOS 9 below version of App Delegate's openURL method detected");
  147. NSString *appBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
  148. #pragma clang diagnostic push
  149. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  150. BOOL handled = [self.appDelegate application:self.mainApplication
  151. openURL:url
  152. sourceApplication:appBundleIdentifier
  153. annotation:@{}];
  154. #pragma clang pop
  155. return handled;
  156. }
  157. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240010",
  158. @"No approriate openURL method defined for App Delegate");
  159. return NO;
  160. }
  161. // Try to handle the url as a universal link by triggering
  162. // application:continueUserActivity:restorationHandler: on App's delegate object directly.
  163. // @returns YES if that delegate method is defined and seeing a YES being returned from
  164. // trigging it
  165. - (BOOL)followURLWithContinueUserActivity:(NSURL *)url {
  166. if (self.isContinueUserActivityMethodDefined) {
  167. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004",
  168. @"App delegate responds to application:continueUserActivity:restorationHandler:."
  169. "Simulating action url opening from a web browser.");
  170. NSUserActivity *userActivity =
  171. [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
  172. userActivity.webpageURL = url;
  173. BOOL handled = [self.appDelegate application:self.mainApplication
  174. continueUserActivity:userActivity
  175. restorationHandler:^(NSArray *restorableObjects) {
  176. // mimic system behavior of triggering restoreUserActivityState:
  177. // method on each element of restorableObjects
  178. for (id nextRestoreObject in restorableObjects) {
  179. if ([nextRestoreObject isKindOfClass:[UIResponder class]]) {
  180. UIResponder *responder = (UIResponder *)nextRestoreObject;
  181. [responder restoreUserActivityState:userActivity];
  182. }
  183. }
  184. }];
  185. if (handled) {
  186. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240005",
  187. @"App handling acton URL returns YES, no more further action taken");
  188. } else {
  189. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App handling acton URL returns NO.");
  190. }
  191. return handled;
  192. } else {
  193. return NO;
  194. }
  195. }
  196. - (void)followURLViaIOS:(NSURL *)url withCompletionBlock:(void (^)(BOOL success))completion {
  197. if ([self.mainApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) {
  198. NSDictionary *options = @{};
  199. [self.mainApplication
  200. openURL:url
  201. options:options
  202. completionHandler:^(BOOL success) {
  203. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240006", @"openURL result is %d", success);
  204. completion(success);
  205. }];
  206. } else {
  207. // fallback to the older version of openURL
  208. BOOL success = [self.mainApplication openURL:url];
  209. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"openURL result is %d", success);
  210. completion(success);
  211. }
  212. }
  213. - (BOOL)isCustomSchemeForCurrentApp:(NSURL *)url {
  214. NSString *schemeInLowerCase = [url.scheme lowercaseString];
  215. return [self.appCustomURLSchemesSet containsObject:schemeInLowerCase];
  216. }
  217. + (BOOL)isHttpOrHttpsScheme:(NSURL *)url {
  218. NSString *schemeInLowerCase = [url.scheme lowercaseString];
  219. return
  220. [schemeInLowerCase isEqualToString:@"https"] || [schemeInLowerCase isEqualToString:@"http"];
  221. }
  222. @end