blob: 50017a130388d9f3c8deb6af947a5e41edbac345 [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/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/open_url_util.h"
#import "ios/chrome/browser/web/mailto_url_rewriter.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"
#include "url/url_constants.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// 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<NSString*>
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;
// Presents an alert controller with |prompt| and |openLabel| as button label
// on the root view controller before launching an external app identified by
// |URL|.
- (void)openExternalAppWithURL:(NSURL*)URL
prompt:(NSString*)prompt
openLabel:(NSString*)openLabel;
@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)openExternalAppWithURL:(NSURL*)URL
prompt:(NSString*)prompt
openLabel:(NSString*)openLabel {
UIAlertController* alertController =
[UIAlertController alertControllerWithTitle:nil
message:prompt
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* openAction =
[UIAlertAction actionWithTitle:openLabel
style:UIAlertActionStyleDefault
handler:^(UIAlertAction* action) {
RecordExternalApplicationOpened(true);
OpenUrlWithCompletionHandler(URL, nil);
}];
UIAlertAction* cancelAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction* action) {
RecordExternalApplicationOpened(false);
}];
[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;
// Don't open external application if chrome is not active.
if ([[UIApplication sharedApplication] applicationState] !=
UIApplicationStateActive)
return NO;
NSURL* URL = net::NSURLWithGURL(gURL);
if (base::ios::IsRunningOnOrLater(10, 3, 0)) {
if (UrlHasAppStoreScheme(gURL)) {
NSString* prompt = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP);
NSString* openLabel =
l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL);
[self openExternalAppWithURL:URL prompt:prompt openLabel:openLabel];
return YES;
}
} else {
// Prior to iOS 10.3, iOS does not prompt user when facetime: and
// facetime-audio: URL schemes are opened, so Chrome needs to present an
// alert before placing a phone call.
if (UrlHasPhoneCallScheme(gURL)) {
[self openExternalAppWithURL:URL
prompt:[[self class] formatCallArgument:URL]
openLabel:PromptActionString([URL scheme])];
return YES;
}
// Prior to iOS 10.3, Chrome prompts user with an alert before opening
// App Store when user did not tap on any links and an iTunes app URL is
// opened. This maintains parity with Safari in pre-10.3 environment.
if (!linkClicked && UrlHasAppStoreScheme(gURL)) {
NSString* prompt = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP);
NSString* openLabel =
l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL);
[self openExternalAppWithURL:URL prompt:prompt openLabel:openLabel];
return YES;
}
}
// Replaces |URL| with a rewritten URL if it is of mailto: scheme.
if (gURL.SchemeIs(url::kMailToScheme)) {
MailtoURLRewriter* rewriter =
[[MailtoURLRewriter alloc] initWithStandardHandlers];
NSString* launchURL = [rewriter rewriteMailtoURL:gURL];
if (launchURL)
URL = [NSURL URLWithString:launchURL];
UMA_HISTOGRAM_BOOLEAN("IOS.MailtoURLRewritten", launchURL != nil);
}
// 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