FIRIAMActionURLFollower.m 11 KB

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