blob: 9489a55ca058771bbbdef29a9697749fa95f123d [file] [log] [blame]
// 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/ios_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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];
});
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);
}
// Returns whether gURL has the scheme of a URL that initiates a call.
BOOL UrlHasPhoneCallScheme(const GURL& gURL) {
return gURL.SchemeIs("tel") || gURL.SchemeIs("facetime") ||
gURL.SchemeIs("facetime-audio");
}
// Returns a string to be used as the label for the prompt's action button.
NSString* PromptActionString(NSString* scheme) {
if ([scheme isEqualToString:@"facetime"])
return l10n_util::GetNSString(IDS_IOS_FACETIME_BUTTON);
else if ([scheme isEqualToString:@"tel"] ||
[scheme isEqualToString:@"facetime-audio"])
return l10n_util::GetNSString(IDS_IOS_PHONE_CALL_BUTTON);
NOTREACHED();
return @"";
}
} // namespace
@interface ExternalAppLauncher ()
// Returns the Phone/FaceTime call argument from |URL|.
+ (NSString*)formatCallArgument:(NSURL*)URL;
// Ask user for confirmation before dialing Phone or FaceTime destinations.
- (void)openPromptForURL:(NSURL*)URL;
// 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)openPromptForURL:(NSURL*)URL {
[self presentAlertControllerWithMessage:[[self class] formatCallArgument:URL]
openTitle:PromptActionString(URL.scheme)
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);
// iOS 10.3 introduced new prompts when facetime: and facetime-audio:
// URL schemes are opened. It is no longer necessary for Chrome to present
// another prompt before the system-provided prompt.
if (!base::ios::IsRunningOnOrLater(10, 3, 0) && UrlHasPhoneCallScheme(gURL)) {
// 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(openPromptForURL:)
withObject:URL
afterDelay:0.0];
return YES;
}
// 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