chromium/ios/chrome/browser/app_launcher/model/app_launcher_browser_agent.mm

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/app_launcher/model/app_launcher_browser_agent.h"

#import "base/check.h"
#import "base/functional/bind.h"
#import "base/metrics/histogram_macros.h"
#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h"
#import "ios/chrome/browser/mailto_handler/model/mailto_handler_service.h"
#import "ios/chrome/browser/mailto_handler/model/mailto_handler_service_factory.h"
#import "ios/chrome/browser/overlays/model/public/overlay_callback_manager.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_queue.h"
#import "ios/chrome/browser/overlays/model/public/overlay_response.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/app_launcher_overlay.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"

BROWSER_USER_DATA_KEY_IMPL(AppLauncherBrowserAgent)

using app_launcher_overlays::AllowAppLaunchResponse;
using app_launcher_overlays::AppLaunchConfirmationRequest;

// A bridge class to observe the SceneState transitions.
@interface AppLauncherSceneStateObserver : NSObject <SceneStateObserver>
- (instancetype)initWithTransitionCallback:(base::RepeatingClosure)callback;
@end

@implementation AppLauncherSceneStateObserver {
  base::RepeatingClosure _transitionCallback;
}

- (instancetype)initWithTransitionCallback:(base::RepeatingClosure)callback {
  self = [super init];
  if (self) {
    _transitionCallback = callback;
  }
  return self;
}

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  _transitionCallback.Run();
}

@end

namespace {
namespace {
// After a successfull launch, will wait for this delay before checking the
// application status. If the application is backgrounded or active at this
// point, call the final completion handler.
// This delay will avoid staying in "no navigation" mode if we miss the
// activation signal (or if the app was never inactivated like opening another
// app in another window on iPad).
static constexpr base::TimeDelta kCheckSceneStatusDelay = base::Seconds(1);
}  // namespace

// Records histogram metric on the user's response when prompted to open another
// application. `user_accepted` should be YES if the user accepted the prompt to
// launch another application. This call is extracted to a separate function to
// reduce macro code expansion.
void RecordAppLaunchRequestMetrics(
    app_launcher_overlays::AppLaunchConfirmationRequestCause cause,
    BOOL user_accepted) {
  switch (cause) {
    case app_launcher_overlays::AppLaunchConfirmationRequestCause::kOther:
      UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened.Generic",
                            user_accepted);
      break;
    case app_launcher_overlays::AppLaunchConfirmationRequestCause::
        kRepeatedRequest:
      UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened.Repeated",
                            user_accepted);
      break;
    case app_launcher_overlays::AppLaunchConfirmationRequestCause::
        kOpenFromIncognito:
      UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened.FromIncognito",
                            user_accepted);
      break;
    case app_launcher_overlays::AppLaunchConfirmationRequestCause::
        kNoUserInteraction:
      UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened.NoUserInteraction",
                            user_accepted);
      break;
    case app_launcher_overlays::AppLaunchConfirmationRequestCause::
        kAppLaunchFailed:
      UMA_HISTOGRAM_BOOLEAN("Tab.ExternalApplicationOpened.Failed",
                            user_accepted);
      break;
  }
}

// Callback for the app launcher alert overlay.
void AppLauncherOverlayCallback(
    base::OnceCallback<void(bool)> completion,
    app_launcher_overlays::AppLaunchConfirmationRequestCause cause,
    OverlayResponse* response) {
  // Check whether the user has allowed the navigation.
  bool user_accepted = response && response->GetInfo<AllowAppLaunchResponse>();

  RecordAppLaunchRequestMetrics(cause, user_accepted);

  // Execute the completion with the response.
  DCHECK(!completion.is_null());
  std::move(completion).Run(user_accepted);
}

// Launches the app for `url` if `user_accepted` is true.
void LaunchExternalApp(const GURL url,
                       base::OnceCallback<void(bool)> completion,
                       bool user_accepted = true) {
  if (!user_accepted) {
    std::move(completion).Run(false);
    return;
  }
  [[UIApplication sharedApplication]
                openURL:net::NSURLWithGURL(url)
                options:@{}
      completionHandler:base::CallbackToBlock(std::move(completion))];
}

app_launcher_overlays::AppLaunchConfirmationRequestCause
RequestCauseFromActionCause(AppLauncherAlertCause cause) {
  switch (cause) {
    case AppLauncherAlertCause::kOther:
      return app_launcher_overlays::AppLaunchConfirmationRequestCause::kOther;
    case AppLauncherAlertCause::kRepeatedLaunchDetected:
      return app_launcher_overlays::AppLaunchConfirmationRequestCause::
          kRepeatedRequest;
    case AppLauncherAlertCause::kOpenFromIncognito:
      return app_launcher_overlays::AppLaunchConfirmationRequestCause::
          kOpenFromIncognito;
    case AppLauncherAlertCause::kNoUserInteraction:
      return app_launcher_overlays::AppLaunchConfirmationRequestCause::
          kNoUserInteraction;
    case AppLauncherAlertCause::kAppLaunchFailed:
      return app_launcher_overlays::AppLaunchConfirmationRequestCause::
          kAppLaunchFailed;
  }
}

}  // namespace

#pragma mark - AppLauncherBrowserAgent

AppLauncherBrowserAgent::AppLauncherBrowserAgent(Browser* browser)
    : tab_helper_delegate_(browser),
      tab_helper_delegate_installer_(&tab_helper_delegate_, browser) {
  browser->AddObserver(this);
  app_launcher_scene_state_observer_ = [[AppLauncherSceneStateObserver alloc]
      initWithTransitionCallback:
          base::BindRepeating(&AppLauncherBrowserAgent::TabHelperDelegate::
                                  SceneActivationLevelChanged,
                              tab_helper_delegate_.AsWeakPtr())];
  [browser->GetSceneState() addObserver:app_launcher_scene_state_observer_];
}

AppLauncherBrowserAgent::~AppLauncherBrowserAgent() = default;

#pragma mark - BrowserObserver

void AppLauncherBrowserAgent::BrowserDestroyed(Browser* browser) {
  [browser->GetSceneState() removeObserver:app_launcher_scene_state_observer_];
  browser->RemoveObserver(this);
}

#pragma mark - AppLauncherBrowserAgent::TabHelperDelegate

AppLauncherBrowserAgent::TabHelperDelegate::TabHelperDelegate(Browser* browser)
    : browser_(browser) {
  DCHECK(browser_);
  DCHECK(browser_->GetWebStateList());
}

AppLauncherBrowserAgent::TabHelperDelegate::~TabHelperDelegate() = default;

base::WeakPtr<AppLauncherBrowserAgent::TabHelperDelegate>
AppLauncherBrowserAgent::TabHelperDelegate::AsWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

#pragma mark AppLauncherTabHelperDelegate

void AppLauncherBrowserAgent::TabHelperDelegate::LaunchAppForTabHelper(
    AppLauncherTabHelper* tab_helper,
    const GURL& url,
    base::OnceCallback<void(bool)> completion,
    base::OnceCallback<void()> back_completion) {
  // Don't open application if the scene is not active or if an app opening is
  // already in progress (should only happen if there are very quick calls to
  // this callback).
  if ([browser_->GetSceneState() activationLevel] !=
          SceneActivationLevelForegroundActive ||
      app_launch_completion_ || back_to_app_completion_) {
    std::move(completion).Run(false);
    return;
  }

  app_launch_completion_ = std::move(completion);
  back_to_app_completion_ = std::move(back_completion);
  base::OnceCallback<void(bool)> launch_completion = base::BindOnce(
      &AppLauncherBrowserAgent::TabHelperDelegate::OnAppLaunchCompleted,
      AsWeakPtr());

  // Uses a Mailto Handler to open the appropriate app.
  if (url.SchemeIs(url::kMailToScheme)) {
    MailtoHandlerServiceFactory::GetForBrowserState(browser_->GetBrowserState())
        ->HandleMailtoURL(net::NSURLWithGURL(url),
                          base::BindOnce(std::move(launch_completion), true));
    return;
  }

  LaunchExternalApp(url, std::move(launch_completion));
}

void AppLauncherBrowserAgent::TabHelperDelegate::ShowAppLaunchAlert(
    AppLauncherTabHelper* tab_helper,
    AppLauncherAlertCause cause,
    base::OnceCallback<void(bool)> completion) {
  std::unique_ptr<OverlayRequest> request =
      OverlayRequest::CreateWithConfig<AppLaunchConfirmationRequest>(
          RequestCauseFromActionCause(cause));
  request->GetCallbackManager()->AddCompletionCallback(
      base::BindOnce(&AppLauncherOverlayCallback, std::move(completion),
                     RequestCauseFromActionCause(cause)));
  GetQueueForAppLaunchDialog(tab_helper->web_state())
      ->AddRequest(std::move(request));
}

#pragma mark Private

OverlayRequestQueue*
AppLauncherBrowserAgent::TabHelperDelegate::GetQueueForAppLaunchDialog(
    web::WebState* web_state) {
  return OverlayRequestQueue::FromWebState(web_state,
                                           OverlayModality::kWebContentArea);
}

// Called in the completion handler of `UIApplication openURL:...`.
void AppLauncherBrowserAgent::TabHelperDelegate::OnAppLaunchCompleted(
    bool success) {
  if (!success) {
    back_to_app_completion_.Reset();
  }
  if (app_launch_completion_) {
    std::move(app_launch_completion_).Run(success);
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      BindOnce(&AppLauncherBrowserAgent::TabHelperDelegate::
                   SceneActivationLevelChanged,
               AsWeakPtr()),
      kCheckSceneStatusDelay);
}

// Called on SceneState activation transitions.
void AppLauncherBrowserAgent::TabHelperDelegate::SceneActivationLevelChanged() {
  if (browser_->GetSceneState().activationLevel !=
          SceneActivationLevelForegroundInactive &&
      back_to_app_completion_) {
    std::move(back_to_app_completion_).Run();
  }
}