blob: ab5ff972dace5f5944e6d3329b307bbec3b40cfd [file] [log] [blame]
// Copyright 2017 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_tab_helper.h"
#include "base/callback.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ui/external_app/open_mail_handler_view_controller.h"
#import "ios/chrome/browser/web/external_app_launcher_util.h"
#import "ios/chrome/browser/web/external_apps_launch_policy_decider.h"
#import "ios/chrome/browser/web/mailto_handler.h"
#import "ios/chrome/browser/web/mailto_handler_manager.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/third_party/material_components_ios/src/components/BottomSheet/src/MDCBottomSheetController.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
DEFINE_WEB_STATE_USER_DATA_KEY(ExternalAppLauncherTabHelper);
namespace {
// Launches the mail client app represented by |handler| and records metrics.
void LaunchMailClientApp(const GURL& url, MailtoHandler* handler) {
NSString* launch_url = [handler rewriteMailtoURL:url];
UMA_HISTOGRAM_BOOLEAN("IOS.MailtoURLRewritten", launch_url != nil);
NSURL* url_to_open = launch_url.length ? [NSURL URLWithString:launch_url]
: net::NSURLWithGURL(url);
if (@available(iOS 10, *)) {
[[UIApplication sharedApplication] openURL:url_to_open
options:@{}
completionHandler:nil];
}
#if !defined(__IPHONE_10_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0
else {
[[UIApplication sharedApplication] openURL:url_to_open];
}
#endif
}
// Shows a prompt for the user to choose which mail client app to use to handle
// a mailto:// URL.
void PromptForMailClientWithUrl(const GURL& url,
MailtoHandlerManager* manager) {
GURL copied_url_to_open = url;
OpenMailHandlerViewController* mail_handler_chooser =
[[OpenMailHandlerViewController alloc]
initWithManager:manager
selectedHandler:^(MailtoHandler* _Nonnull handler) {
LaunchMailClientApp(copied_url_to_open, handler);
}];
MDCBottomSheetController* bottom_sheet = [[MDCBottomSheetController alloc]
initWithContentViewController:mail_handler_chooser];
[[[[UIApplication sharedApplication] keyWindow] rootViewController]
presentViewController:bottom_sheet
animated:YES
completion:nil];
}
// Presents an alert controller on the root view controller with |prompt| as
// body text, |accept label| and |reject label| as button labels, and
// a non null |responseHandler| that takes a boolean to handle user response.
void ShowExternalAppLauncherPrompt(NSString* prompt,
NSString* accept_label,
NSString* reject_label,
base::OnceCallback<void(bool)> callback) {
__block base::OnceCallback<void(bool)> block_callback = std::move(callback);
UIAlertController* alert_controller =
[UIAlertController alertControllerWithTitle:nil
message:prompt
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* accept_action =
[UIAlertAction actionWithTitle:accept_label
style:UIAlertActionStyleDefault
handler:^(UIAlertAction* action) {
std::move(block_callback).Run(true);
}];
UIAlertAction* reject_action =
[UIAlertAction actionWithTitle:reject_label
style:UIAlertActionStyleCancel
handler:^(UIAlertAction* action) {
std::move(block_callback).Run(false);
}];
[alert_controller addAction:reject_action];
[alert_controller addAction:accept_action];
[[[[UIApplication sharedApplication] keyWindow] rootViewController]
presentViewController:alert_controller
animated:YES
completion:nil];
}
// Launches external app identified by |url| if |accept| is true.
void LaunchExternalApp(NSURL* url, bool accept) {
UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened", accept);
if (accept) {
[[UIApplication sharedApplication] openURL:url
options:@{}
completionHandler:nil];
}
}
// Presents an alert controller with |prompt| and |open_label| as button label
// on the root view controller before launching an external app identified by
// |url|.
void OpenExternalAppWithUrl(NSURL* url,
NSString* prompt,
NSString* open_label) {
ShowExternalAppLauncherPrompt(
prompt, /*accept_label=*/open_label,
/*reject_label=*/l10n_util::GetNSString(IDS_CANCEL),
base::BindOnce(&LaunchExternalApp, url));
}
// Opens URL in an external application if possible (optionally after
// confirming via dialog in case that user didn't interact using
// |link_clicked| or if the external application is face time) or returns NO
// if there is no such application available.
bool OpenUrl(const GURL& gurl, bool link_clicked) {
// Don't open external application if chrome is not active.
if ([[UIApplication sharedApplication] applicationState] !=
UIApplicationStateActive) {
return NO;
}
NSURL* url = net::NSURLWithGURL(gurl);
if (@available(iOS 10.3, *)) {
if (UrlHasAppStoreScheme(gurl)) {
NSString* prompt = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP);
NSString* open_label =
l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL);
OpenExternalAppWithUrl(url, prompt, open_label);
return true;
}
} 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)) {
OpenExternalAppWithUrl(
url, /*prompt=*/GetFormattedAbsoluteUrlWithSchemeRemoved(url),
/*open_label=*/GetPromptActionString(url.scheme));
return true;
}
// 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 (!link_clicked && UrlHasAppStoreScheme(gurl)) {
NSString* prompt = l10n_util::GetNSString(IDS_IOS_OPEN_IN_ANOTHER_APP);
NSString* open_label =
l10n_util::GetNSString(IDS_IOS_APP_LAUNCHER_OPEN_APP_BUTTON_LABEL);
OpenExternalAppWithUrl(url, prompt, open_label);
return true;
}
}
// Replaces |url| with a rewritten URL if it is of mailto: scheme.
if (gurl.SchemeIs(url::kMailToScheme)) {
MailtoHandlerManager* manager =
[MailtoHandlerManager mailtoHandlerManagerWithStandardHandlers];
NSString* handler_id = manager.defaultHandlerID;
if (!handler_id) {
PromptForMailClientWithUrl(gurl, manager);
return true;
}
MailtoHandler* handler = [manager defaultHandlerByID:handler_id];
LaunchMailClientApp(gurl, handler);
return true;
}
// If the following call returns YES, an external application is about to be
// launched and Chrome will go into the background now.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// TODO(crbug.com/774736): This call still needs to be
// updated. It's heavily nested so some refactoring is needed.
return [[UIApplication sharedApplication] openURL:url];
#pragma clang diagnostic pop
}
} // namespace
ExternalAppLauncherTabHelper::ExternalAppLauncherTabHelper(
web::WebState* web_state)
: policy_decider_([[ExternalAppsLaunchPolicyDecider alloc] init]),
weak_factory_(this) {}
ExternalAppLauncherTabHelper::~ExternalAppLauncherTabHelper() = default;
void ExternalAppLauncherTabHelper::HandleRepeatedAttemptsToLaunch(
const GURL& url,
const GURL& source_page_url,
bool allowed) {
if (allowed) {
// By confirming that user wants to launch the
// application, there is no need to check for
// |link_clicked|.
OpenUrl(url, /*link_clicked=*/true);
} else {
// TODO(crbug.com/674649): Once non modal
// dialogs are implemented, update this to
// always prompt instead of blocking the app.
[policy_decider_ blockLaunchingAppURL:url
fromSourcePageURL:source_page_url];
}
UMA_HISTOGRAM_BOOLEAN("IOS.RepeatedExternalAppPromptResponse", allowed);
is_prompt_active_ = false;
}
bool ExternalAppLauncherTabHelper::RequestToOpenUrl(const GURL& url,
const GURL& source_page_url,
bool link_clicked) {
if (!url.is_valid() || !url.has_scheme())
return false;
// Don't open external application if chrome is not active.
if ([[UIApplication sharedApplication] applicationState] !=
UIApplicationStateActive) {
return false;
}
// Don't try to open external application if a prompt is already active.
if (is_prompt_active_)
return false;
[policy_decider_ didRequestLaunchExternalAppURL:url
fromSourcePageURL:source_page_url];
ExternalAppLaunchPolicy policy =
[policy_decider_ launchPolicyForURL:url
fromSourcePageURL:source_page_url];
switch (policy) {
case ExternalAppLaunchPolicyBlock: {
return false;
}
case ExternalAppLaunchPolicyAllow: {
return OpenUrl(url, link_clicked);
}
case ExternalAppLaunchPolicyPrompt: {
is_prompt_active_ = true;
NSString* prompt_body =
l10n_util::GetNSString(IDS_IOS_OPEN_REPEATEDLY_ANOTHER_APP);
NSString* allow_label =
l10n_util::GetNSString(IDS_IOS_OPEN_REPEATEDLY_ANOTHER_APP_ALLOW);
NSString* block_label =
l10n_util::GetNSString(IDS_IOS_OPEN_REPEATEDLY_ANOTHER_APP_BLOCK);
base::OnceCallback<void(bool)> callback = base::BindOnce(
&ExternalAppLauncherTabHelper::HandleRepeatedAttemptsToLaunch,
weak_factory_.GetWeakPtr(), url, source_page_url);
ShowExternalAppLauncherPrompt(prompt_body, allow_label, block_label,
std::move(callback));
return true;
}
}
}