OLD | NEW |
(Empty) | |
| 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/web/external_app_launcher.h" |
| 6 |
| 7 #include "base/ios/weak_nsobject.h" |
| 8 #include "base/logging.h" |
| 9 #include "base/metrics/histogram.h" |
| 10 #include "base/strings/sys_string_conversions.h" |
| 11 #include "components/strings/grit/components_strings.h" |
| 12 #include "ios/chrome/browser/experimental_flags.h" |
| 13 #import "ios/chrome/browser/open_url_util.h" |
| 14 #include "ios/chrome/grit/ios_strings.h" |
| 15 #import "net/base/mac/url_conversions.h" |
| 16 #include "ui/base/l10n/l10n_util.h" |
| 17 #include "url/gurl.h" |
| 18 |
| 19 namespace { |
| 20 |
| 21 typedef void (^AlertHandler)(UIAlertAction* action); |
| 22 |
| 23 // Returns a set of NSStrings that are URL schemes for iTunes Stores. |
| 24 NSSet<NSString*>* ITMSSchemes() { |
| 25 static NSSet<NSString*>* schemes; |
| 26 static dispatch_once_t once; |
| 27 dispatch_once(&once, ^{ |
| 28 schemes = |
| 29 [[NSSet setWithObjects:@"itms", @"itmss", @"itms-apps", @"itms-appss", |
| 30 // There's no evidence that itms-bookss is |
| 31 // actually supported, but over-inclusion |
| 32 // costs less than under-inclusion. |
| 33 @"itms-books", @"itms-bookss", nil] retain]; |
| 34 }); |
| 35 return schemes; |
| 36 } |
| 37 |
| 38 bool UrlHasAppStoreScheme(const GURL& gURL) { |
| 39 std::string scheme = gURL.scheme(); |
| 40 return [ITMSSchemes() containsObject:base::SysUTF8ToNSString(scheme)]; |
| 41 } |
| 42 |
| 43 // Logs an entry for |Tab.ExternalApplicationOpened|. If the user decided to |
| 44 // open in an external app, pass true. Otherwise, if the user cancelled the |
| 45 // opening, pass false. |
| 46 void RecordExternalApplicationOpened(bool opened) { |
| 47 UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened", opened); |
| 48 } |
| 49 |
| 50 } // namespace |
| 51 |
| 52 @interface ExternalAppLauncher () |
| 53 // Returns the Phone/FaceTime call argument from |URL|. |
| 54 + (NSString*)formatCallArgument:(NSURL*)URL; |
| 55 // Ask user for confirmation before dialing FaceTime destination. |
| 56 - (void)openFaceTimePromptForURL:(NSURL*)telURL; |
| 57 // Ask user for confirmation before moving to external app. |
| 58 - (void)openExternalAppWithPromptForURL:(NSURL*)URL; |
| 59 // Presents a configured alert controller on the root view controller. |
| 60 - (void)presentAlertControllerWithMessage:(NSString*)message |
| 61 openTitle:(NSString*)openTitle |
| 62 openHandler:(AlertHandler)openHandler |
| 63 cancelHandler:(AlertHandler)cancelHandler; |
| 64 @end |
| 65 |
| 66 @implementation ExternalAppLauncher |
| 67 |
| 68 + (NSString*)formatCallArgument:(NSURL*)URL { |
| 69 NSCharacterSet* charSet = |
| 70 [NSCharacterSet characterSetWithCharactersInString:@"/"]; |
| 71 NSString* scheme = [NSString stringWithFormat:@"%@:", [URL scheme]]; |
| 72 NSString* URLString = [URL absoluteString]; |
| 73 if ([URLString length] <= [scheme length]) |
| 74 return URLString; |
| 75 NSString* prompt = [[[[URL absoluteString] substringFromIndex:[scheme length]] |
| 76 stringByTrimmingCharactersInSet:charSet] stringByRemovingPercentEncoding]; |
| 77 // Returns original URL string if there's nothing interesting to display |
| 78 // other than the scheme itself. |
| 79 if (![prompt length]) |
| 80 return URLString; |
| 81 return prompt; |
| 82 } |
| 83 |
| 84 - (void)openExternalAppWithPromptForURL:(NSURL*)URL { |
| 85 NSString* message = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP); |
| 86 NSString* openTitle = |
| 87 l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL); |
| 88 [self presentAlertControllerWithMessage:message |
| 89 openTitle:openTitle |
| 90 openHandler:^(UIAlertAction* action) { |
| 91 RecordExternalApplicationOpened(true); |
| 92 OpenUrlWithCompletionHandler(URL, nil); |
| 93 } |
| 94 cancelHandler:^(UIAlertAction* action) { |
| 95 RecordExternalApplicationOpened(false); |
| 96 }]; |
| 97 } |
| 98 |
| 99 - (void)openFaceTimePromptForURL:(NSURL*)URL { |
| 100 NSString* openTitle = l10n_util::GetNSString(IDS_IOS_FACETIME_BUTTON); |
| 101 [self presentAlertControllerWithMessage:[[self class] formatCallArgument:URL] |
| 102 openTitle:openTitle |
| 103 openHandler:^(UIAlertAction* action) { |
| 104 OpenUrlWithCompletionHandler(URL, nil); |
| 105 } |
| 106 cancelHandler:nil]; |
| 107 } |
| 108 |
| 109 - (void)presentAlertControllerWithMessage:(NSString*)message |
| 110 openTitle:(NSString*)openTitle |
| 111 openHandler:(AlertHandler)openHandler |
| 112 cancelHandler:(AlertHandler)cancelHandler { |
| 113 UIAlertController* alertController = |
| 114 [UIAlertController alertControllerWithTitle:nil |
| 115 message:message |
| 116 preferredStyle:UIAlertControllerStyleAlert]; |
| 117 UIAlertAction* openAction = |
| 118 [UIAlertAction actionWithTitle:openTitle |
| 119 style:UIAlertActionStyleDefault |
| 120 handler:openHandler]; |
| 121 UIAlertAction* cancelAction = |
| 122 [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| 123 style:UIAlertActionStyleCancel |
| 124 handler:cancelHandler]; |
| 125 [alertController addAction:cancelAction]; |
| 126 [alertController addAction:openAction]; |
| 127 |
| 128 [[[[UIApplication sharedApplication] keyWindow] rootViewController] |
| 129 presentViewController:alertController |
| 130 animated:YES |
| 131 completion:nil]; |
| 132 } |
| 133 |
| 134 - (BOOL)openURL:(const GURL&)gURL linkClicked:(BOOL)linkClicked { |
| 135 if (!gURL.is_valid() || !gURL.has_scheme()) |
| 136 return NO; |
| 137 NSURL* URL = net::NSURLWithGURL(gURL); |
| 138 |
| 139 if (gURL.SchemeIs("facetime") || gURL.SchemeIs("facetime-audio")) { |
| 140 // Showing an alert view immediately has a side-effect where focus is |
| 141 // taken from the UIWebView so quickly that mouseup events are lost and |
| 142 // buttons get 'stuck' in the on position. The solution is to defer |
| 143 // showing the view. |
| 144 [self performSelector:@selector(openFaceTimePromptForURL:) |
| 145 withObject:URL |
| 146 afterDelay:0.0]; |
| 147 return YES; |
| 148 } |
| 149 |
| 150 // Use telprompt instead of tel because telprompt returns user back to |
| 151 // Chrome after phone call is completed/aborted. |
| 152 if (gURL.SchemeIs("tel")) { |
| 153 GURL::Replacements replacements; |
| 154 replacements.SetSchemeStr("telprompt"); |
| 155 URL = net::NSURLWithGURL(gURL.ReplaceComponents(replacements)); |
| 156 DCHECK([[URL scheme] isEqualToString:@"telprompt"]); |
| 157 } |
| 158 |
| 159 // Don't open external application if chrome is not active. |
| 160 if ([[UIApplication sharedApplication] applicationState] != |
| 161 UIApplicationStateActive) |
| 162 return NO; |
| 163 |
| 164 if (experimental_flags::IsExternalApplicationPromptEnabled()) { |
| 165 // Prompt user to open itunes when opening it is not a result of a link |
| 166 // click. |
| 167 if (!linkClicked && UrlHasAppStoreScheme(gURL)) { |
| 168 [self performSelector:@selector(openExternalAppWithPromptForURL:) |
| 169 withObject:URL |
| 170 afterDelay:0.0]; |
| 171 return YES; |
| 172 } |
| 173 } |
| 174 |
| 175 // If the following call returns YES, an external application is about to be |
| 176 // launched and Chrome will go into the background now. |
| 177 // TODO(crbug.com/622735): This call still needs to be updated. |
| 178 // It's heavily nested so some refactoring is needed. |
| 179 return [[UIApplication sharedApplication] openURL:URL]; |
| 180 } |
| 181 |
| 182 @end |
OLD | NEW |