Index: ios/chrome/browser/web/external_app_launcher.mm |
diff --git a/ios/chrome/browser/web/external_app_launcher.mm b/ios/chrome/browser/web/external_app_launcher.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0af7de42663d0e9e5b2bf8977f752db1ad5420ce |
--- /dev/null |
+++ b/ios/chrome/browser/web/external_app_launcher.mm |
@@ -0,0 +1,182 @@ |
+// Copyright 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/web/external_app_launcher.h" |
+ |
+#include "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/metrics/histogram.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/strings/grit/components_strings.h" |
+#include "ios/chrome/browser/experimental_flags.h" |
+#import "ios/chrome/browser/open_url_util.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "ui/base/l10n/l10n_util.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+typedef void (^AlertHandler)(UIAlertAction* action); |
+ |
+// Returns a set of NSStrings that are URL schemes for iTunes Stores. |
+NSSet<NSString*>* ITMSSchemes() { |
+ static NSSet<NSString*>* schemes; |
+ static dispatch_once_t once; |
+ dispatch_once(&once, ^{ |
+ schemes = |
+ [[NSSet setWithObjects:@"itms", @"itmss", @"itms-apps", @"itms-appss", |
+ // There's no evidence that itms-bookss is |
+ // actually supported, but over-inclusion |
+ // costs less than under-inclusion. |
+ @"itms-books", @"itms-bookss", nil] retain]; |
+ }); |
+ return schemes; |
+} |
+ |
+bool UrlHasAppStoreScheme(const GURL& gURL) { |
+ std::string scheme = gURL.scheme(); |
+ return [ITMSSchemes() containsObject:base::SysUTF8ToNSString(scheme)]; |
+} |
+ |
+// Logs an entry for |Tab.ExternalApplicationOpened|. If the user decided to |
+// open in an external app, pass true. Otherwise, if the user cancelled the |
+// opening, pass false. |
+void RecordExternalApplicationOpened(bool opened) { |
+ UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened", opened); |
+} |
+ |
+} // namespace |
+ |
+@interface ExternalAppLauncher () |
+// Returns the Phone/FaceTime call argument from |URL|. |
++ (NSString*)formatCallArgument:(NSURL*)URL; |
+// Ask user for confirmation before dialing FaceTime destination. |
+- (void)openFaceTimePromptForURL:(NSURL*)telURL; |
+// Ask user for confirmation before moving to external app. |
+- (void)openExternalAppWithPromptForURL:(NSURL*)URL; |
+// Presents a configured alert controller on the root view controller. |
+- (void)presentAlertControllerWithMessage:(NSString*)message |
+ openTitle:(NSString*)openTitle |
+ openHandler:(AlertHandler)openHandler |
+ cancelHandler:(AlertHandler)cancelHandler; |
+@end |
+ |
+@implementation ExternalAppLauncher |
+ |
++ (NSString*)formatCallArgument:(NSURL*)URL { |
+ NSCharacterSet* charSet = |
+ [NSCharacterSet characterSetWithCharactersInString:@"/"]; |
+ NSString* scheme = [NSString stringWithFormat:@"%@:", [URL scheme]]; |
+ NSString* URLString = [URL absoluteString]; |
+ if ([URLString length] <= [scheme length]) |
+ return URLString; |
+ NSString* prompt = [[[[URL absoluteString] substringFromIndex:[scheme length]] |
+ stringByTrimmingCharactersInSet:charSet] stringByRemovingPercentEncoding]; |
+ // Returns original URL string if there's nothing interesting to display |
+ // other than the scheme itself. |
+ if (![prompt length]) |
+ return URLString; |
+ return prompt; |
+} |
+ |
+- (void)openExternalAppWithPromptForURL:(NSURL*)URL { |
+ NSString* message = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP); |
+ NSString* openTitle = |
+ l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL); |
+ [self presentAlertControllerWithMessage:message |
+ openTitle:openTitle |
+ openHandler:^(UIAlertAction* action) { |
+ RecordExternalApplicationOpened(true); |
+ OpenUrlWithCompletionHandler(URL, nil); |
+ } |
+ cancelHandler:^(UIAlertAction* action) { |
+ RecordExternalApplicationOpened(false); |
+ }]; |
+} |
+ |
+- (void)openFaceTimePromptForURL:(NSURL*)URL { |
+ NSString* openTitle = l10n_util::GetNSString(IDS_IOS_FACETIME_BUTTON); |
+ [self presentAlertControllerWithMessage:[[self class] formatCallArgument:URL] |
+ openTitle:openTitle |
+ openHandler:^(UIAlertAction* action) { |
+ OpenUrlWithCompletionHandler(URL, nil); |
+ } |
+ cancelHandler:nil]; |
+} |
+ |
+- (void)presentAlertControllerWithMessage:(NSString*)message |
+ openTitle:(NSString*)openTitle |
+ openHandler:(AlertHandler)openHandler |
+ cancelHandler:(AlertHandler)cancelHandler { |
+ UIAlertController* alertController = |
+ [UIAlertController alertControllerWithTitle:nil |
+ message:message |
+ preferredStyle:UIAlertControllerStyleAlert]; |
+ UIAlertAction* openAction = |
+ [UIAlertAction actionWithTitle:openTitle |
+ style:UIAlertActionStyleDefault |
+ handler:openHandler]; |
+ UIAlertAction* cancelAction = |
+ [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
+ style:UIAlertActionStyleCancel |
+ handler:cancelHandler]; |
+ [alertController addAction:cancelAction]; |
+ [alertController addAction:openAction]; |
+ |
+ [[[[UIApplication sharedApplication] keyWindow] rootViewController] |
+ presentViewController:alertController |
+ animated:YES |
+ completion:nil]; |
+} |
+ |
+- (BOOL)openURL:(const GURL&)gURL linkClicked:(BOOL)linkClicked { |
+ if (!gURL.is_valid() || !gURL.has_scheme()) |
+ return NO; |
+ NSURL* URL = net::NSURLWithGURL(gURL); |
+ |
+ if (gURL.SchemeIs("facetime") || gURL.SchemeIs("facetime-audio")) { |
+ // Showing an alert view immediately has a side-effect where focus is |
+ // taken from the UIWebView so quickly that mouseup events are lost and |
+ // buttons get 'stuck' in the on position. The solution is to defer |
+ // showing the view. |
+ [self performSelector:@selector(openFaceTimePromptForURL:) |
+ withObject:URL |
+ afterDelay:0.0]; |
+ return YES; |
+ } |
+ |
+ // Use telprompt instead of tel because telprompt returns user back to |
+ // Chrome after phone call is completed/aborted. |
+ if (gURL.SchemeIs("tel")) { |
+ GURL::Replacements replacements; |
+ replacements.SetSchemeStr("telprompt"); |
+ URL = net::NSURLWithGURL(gURL.ReplaceComponents(replacements)); |
+ DCHECK([[URL scheme] isEqualToString:@"telprompt"]); |
+ } |
+ |
+ // Don't open external application if chrome is not active. |
+ if ([[UIApplication sharedApplication] applicationState] != |
+ UIApplicationStateActive) |
+ return NO; |
+ |
+ if (experimental_flags::IsExternalApplicationPromptEnabled()) { |
+ // Prompt user to open itunes when opening it is not a result of a link |
+ // click. |
+ if (!linkClicked && UrlHasAppStoreScheme(gURL)) { |
+ [self performSelector:@selector(openExternalAppWithPromptForURL:) |
+ withObject:URL |
+ afterDelay:0.0]; |
+ return YES; |
+ } |
+ } |
+ |
+ // If the following call returns YES, an external application is about to be |
+ // launched and Chrome will go into the background now. |
+ // TODO(crbug.com/622735): This call still needs to be updated. |
+ // It's heavily nested so some refactoring is needed. |
+ return [[UIApplication sharedApplication] openURL:URL]; |
+} |
+ |
+@end |