chromium/chrome/browser/ui/ash/new_window/chrome_new_window_client.cc

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

#include "chrome/browser/ui/ash/new_window/chrome_new_window_client.h"
#include "base/memory/raw_ptr.h"

#include <string>
#include <utility>
#include <vector>

#include "apps/launcher.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/internal_app_id_constants.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/app_service/metrics/app_service_metrics.h"
#include "chrome/browser/ash/apps/apk_web_app_service.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/url_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/system_web_apps/apps/calculator_app/calculator_app_utils.h"
#include "chrome/browser/ash/system_web_apps/apps/camera_app/chrome_camera_app_ui_delegate.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/chromeos/arc/arc_web_contents_data.h"
#include "chrome/browser/chromeos/network/network_portal_signin_window.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/app_window_base.h"
#include "chrome/browser/ui/ash/shelf/app_window_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/extensions/app_launch_params.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/webui/chrome_web_contents_handler.h"
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui_util.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/sessions/core/tab_restore_service.h"
#include "components/sessions/core/tab_restore_service_observer.h"
#include "components/url_formatter/url_fixer.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "third_party/blink/public/mojom/navigation/was_activated_option.mojom.h"
#include "ui/aura/window.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "url/url_constants.h"

namespace {

void RestoreTabUsingProfile(Profile* profile) {
  sessions::TabRestoreService* service =
      TabRestoreServiceFactory::GetForProfile(profile);
  service->RestoreMostRecentEntry(nullptr);
}

bool IsIncognitoAllowed() {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  return profile && IncognitoModePrefs::IsIncognitoAllowed(profile);
}

// Returns URL path and query without the "/" prefix. For example, for the URL
// "chrome://settings/networks/?type=WiFi" returns "networks/?type=WiFi".
std::string GetPathAndQuery(const GURL& url) {
  std::string result = url.path();
  if (!result.empty() && result[0] == '/')
    result.erase(0, 1);
  if (url.has_query()) {
    result += '?';
    result += url.query();
  }
  return result;
}

feedback::FeedbackSource MapToChromeSource(
    ash::NewWindowDelegate::FeedbackSource source) {
  switch (source) {
    case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceAsh:
      return feedback::FeedbackSource::kFeedbackSourceAsh;
    case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceAssistant:
      return feedback::FeedbackSource::kFeedbackSourceAssistant;
    case ash::NewWindowDelegate::FeedbackSource::kFeedbackSourceQuickAnswers:
      return feedback::FeedbackSource::kFeedbackSourceQuickAnswers;
    case ash::NewWindowDelegate::FeedbackSource::
        kFeedbackSourceChannelIndicator:
      return feedback::FeedbackSource::kFeedbackSourceChannelIndicator;
  }
}

// When the Files SWA is enabled: Open Files SWA.
// Returns true if it opens the SWA.
// `target_directory` is optional, if provided it opens the Files SWA in the
// given directory, instead of the default directory.
bool OpenFilesSwa(Profile* const profile,
                  base::FilePath target_directory = {}) {
  GURL directory_url;
  if (!target_directory.empty() &&
      !file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
          profile, target_directory, file_manager::util::GetFileManagerURL(),
          &directory_url)) {
    LOG(WARNING) << "Failed to convert the path to FileSystemURL: "
                 << target_directory << " using the default directory";
  }

  std::u16string title;
  ui::SelectFileDialog::FileTypeInfo file_type_info;
  file_type_info.allowed_paths =
      ui::SelectFileDialog::FileTypeInfo::ANY_PATH_OR_URL;
  GURL files_swa_url =
      ::file_manager::util::GetFileManagerMainPageUrlWithParams(
          ui::SelectFileDialog::SELECT_NONE, title,
          /*current_directory_url=*/directory_url,
          /*selection_url=*/{},
          /*target_name=*/{}, &file_type_info,
          /*file_type_index=*/0,
          /*search_query=*/{},
          /*show_android_picker_apps=*/false,
          /*volume_filter=*/{});

  ash::SystemAppLaunchParams params;
  params.url = files_swa_url;
  ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::FILE_MANAGER,
                               params);
  return true;
}

}  // namespace

ChromeNewWindowClient::ChromeNewWindowClient() {
  arc::ArcIntentHelperBridge::SetControlCameraAppDelegate(this);
}

ChromeNewWindowClient::~ChromeNewWindowClient() {
  arc::ArcIntentHelperBridge::SetControlCameraAppDelegate(nullptr);
}

// static
ChromeNewWindowClient* ChromeNewWindowClient::Get() {
  return static_cast<ChromeNewWindowClient*>(
      ash::NewWindowDelegate::GetInstance());
}

// TabRestoreHelper is used to restore a tab. In particular when the user
// attempts to a restore a tab if the TabRestoreService hasn't finished loading
// this waits for it. Once the TabRestoreService finishes loading the tab is
// restored.
class ChromeNewWindowClient::TabRestoreHelper
    : public sessions::TabRestoreServiceObserver {
 public:
  TabRestoreHelper(ChromeNewWindowClient* delegate,
                   Profile* profile,
                   sessions::TabRestoreService* service)
      : delegate_(delegate), profile_(profile), tab_restore_service_(service) {
    tab_restore_service_->AddObserver(this);
  }

  TabRestoreHelper(const TabRestoreHelper&) = delete;
  TabRestoreHelper& operator=(const TabRestoreHelper&) = delete;

  ~TabRestoreHelper() override { tab_restore_service_->RemoveObserver(this); }

  sessions::TabRestoreService* tab_restore_service() {
    return tab_restore_service_;
  }

  void TabRestoreServiceChanged(sessions::TabRestoreService* service) override {
  }

  void TabRestoreServiceDestroyed(
      sessions::TabRestoreService* service) override {
    // This destroys us.
    delegate_->tab_restore_helper_.reset();
  }

  void TabRestoreServiceLoaded(sessions::TabRestoreService* service) override {
    RestoreTabUsingProfile(profile_);
    // This destroys us.
    delegate_->tab_restore_helper_.reset();
  }

 private:
  raw_ptr<ChromeNewWindowClient> delegate_;
  raw_ptr<Profile> profile_;
  raw_ptr<sessions::TabRestoreService> tab_restore_service_;
};

void ChromeNewWindowClient::NewTab() {
  Browser* browser = chrome::FindBrowserWithActiveWindow();
  if (browser && browser->is_type_normal()) {
    chrome::NewTab(browser);
    return;
  }

  // Display a browser, setting the focus to the location bar after it is shown.
  {
    Profile* profile = ProfileManager::GetActiveUserProfile();
    bool is_otr_forced =
        IncognitoModePrefs::ShouldOpenSubsequentBrowsersInIncognito(
            *base::CommandLine::ForCurrentProcess(), profile->GetPrefs());

    if (is_otr_forced) {
      profile = profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
    }
    chrome::ScopedTabbedBrowserDisplayer displayer(profile);
    browser = displayer.browser();
    chrome::NewTab(browser);
  }

  browser->SetFocusToLocationBar();
}

void ChromeNewWindowClient::NewWindow(bool is_incognito,
                                      bool should_trigger_session_restore) {
  if (is_incognito && !IsIncognitoAllowed())
    return;

  Browser* browser = chrome::FindBrowserWithActiveWindow();
  Profile* profile = (browser && browser->profile())
                         ? browser->profile()->GetOriginalProfile()
                         : ProfileManager::GetActiveUserProfile();
  chrome::NewEmptyWindow(
      is_incognito ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
                   : profile,
      should_trigger_session_restore);
}

void ChromeNewWindowClient::NewWindowForDetachingTab(
    aura::Window* source_window,
    const ui::OSExchangeData& drop_data,
    NewWindowForDetachingTabCallback closure) {
  BrowserView* source_view = BrowserView::GetBrowserViewForNativeWindow(
      source_window->GetToplevelWindow());
  if (!source_view) {
    std::move(closure).Run(/*new_window=*/nullptr);
    return;
  }

  Browser::CreateParams params = source_view->browser()->create_params();
  params.user_gesture = true;
  params.initial_show_state = ui::SHOW_STATE_DEFAULT;
  Browser* browser = Browser::Create(params);
  if (!browser) {
    std::move(closure).Run(/*new_window=*/nullptr);
    return;
  }

  if (!tab_strip_ui::DropTabsInNewBrowser(browser, drop_data)) {
    browser->window()->Close();
    std::move(closure).Run(/*new_window=*/nullptr);
    return;
  }

  // TODO(crbug.com/40126106): evaluate whether the above
  // failures can happen in valid states, and if so whether we need to
  // reflect failure in UX.

  // TODO(crbug.com/1225667): Loosen restriction for SplitViewController to be
  // able to snap a window without calling Show(). It will simplify the logic
  // without having to set and clear ash::kIsDraggingTabsKey by calling Show()
  // after snapping the window to the right place.

  // We need to mark the newly created window with |ash::kIsDraggingTabsKey|
  // and clear it afterwards in order to prevent
  // SplitViewController::AutoSnapController from snapping it on Show().
  aura::Window* window = browser->window()->GetNativeWindow();
  window->SetProperty(ash::kIsDraggingTabsKey, true);
  browser->window()->Show();
  window->ClearProperty(ash::kIsDraggingTabsKey);
  std::move(closure).Run(window);
}

namespace {
WindowOpenDisposition ToWindowOpenDisposition(
    ash::NewWindowDelegate::Disposition disposition) {
  switch (disposition) {
    case ash::NewWindowDelegate::Disposition::kNewForegroundTab:
      return WindowOpenDisposition::NEW_FOREGROUND_TAB;
    case ash::NewWindowDelegate::Disposition::kNewWindow:
      return WindowOpenDisposition::NEW_WINDOW;
    case ash::NewWindowDelegate::Disposition::kOffTheRecord:
      return WindowOpenDisposition::OFF_THE_RECORD;
    case ash::NewWindowDelegate::Disposition::kSwitchToTab:
      return WindowOpenDisposition::SWITCH_TO_TAB;
  }
}
}  // namespace

void ChromeNewWindowClient::OpenUrl(const GURL& url,
                                    OpenUrlFrom from,
                                    Disposition disposition) {
  // Opens a URL in a new tab. If the URL is for a chrome://settings page,
  // opens settings in a new window.
  Profile* profile = ProfileManager::GetActiveUserProfile();
  if ((url.SchemeIs(url::kAboutScheme) ||
       url.SchemeIs(content::kChromeUIScheme))) {
    // Show browser settings (e.g. chrome://settings). This may open in a window
    // or a tab depending on feature SplitSettings.
    if (url.host() == chrome::kChromeUISettingsHost) {
      std::string sub_page = GetPathAndQuery(url);
      chrome::ShowSettingsSubPageForProfile(profile, sub_page);
      return;
    }
    // OS settings are shown in a window.
    if (url.host() == chrome::kChromeUIOSSettingsHost) {
      std::string sub_page = GetPathAndQuery(url);
      chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile,
                                                                   sub_page);
      return;
    }
  }

  NavigateParams navigate_params(
      profile, url,
      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                ui::PAGE_TRANSITION_FROM_API));
  navigate_params.disposition = ToWindowOpenDisposition(disposition);

  // If the |from| is kUserInteraction, then the page will load with a user
  // activation. This means it will be able to autoplay media without
  // restriction.
  if (from == OpenUrlFrom::kUserInteraction)
    navigate_params.was_activated = blink::mojom::WasActivatedOption::kYes;

  Navigate(&navigate_params);

  if (navigate_params.browser) {
    // The browser window might be on another user's desktop, and hence not
    // visible. Ensure the browser becomes visible on this user's desktop.
    multi_user_util::MoveWindowToCurrentDesktop(
        navigate_params.browser->window()->GetNativeWindow());
  }

  auto* tab = navigate_params.navigated_or_inserted_contents.get();
  if (from == OpenUrlFrom::kArc && tab) {
    // Add a flag to remember this tab originated in the ARC context.
    tab->SetUserData(&arc::ArcWebContentsData::kArcTransitionFlag,
                     std::make_unique<arc::ArcWebContentsData>(tab));
  }
}

void ChromeNewWindowClient::OpenCalculator() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  apps::AppServiceProxy* proxy =
      apps::AppServiceProxyFactory::GetForProfile(profile);
  DCHECK(proxy);
  proxy->Launch(ash::calculator_app::GetInstalledCalculatorAppId(profile),
                ui::EF_NONE, apps::LaunchSource::kFromKeyboard);
}

void ChromeNewWindowClient::OpenFileManager() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  if (OpenFilesSwa(profile)) {
    return;
  }

  apps::AppServiceProxy* proxy =
      apps::AppServiceProxyFactory::GetForProfile(profile);
  DCHECK(proxy);

  auto launch_files_app = [proxy](const apps::AppUpdate& update) {
    if (update.Readiness() != apps::Readiness::kReady) {
      LOG(WARNING)
          << "Couldn't launch Files app because it isn't ready, readiness: "
          << static_cast<int>(update.Readiness());
      return;
    }

    proxy->Launch(update.AppId(),
                  apps::GetEventFlags(WindowOpenDisposition::NEW_FOREGROUND_TAB,
                                      /*prefer_container=*/true),
                  apps::LaunchSource::kFromKeyboard);
  };

  bool result = proxy->AppRegistryCache().ForOneApp(
      file_manager::kFileManagerAppId, std::move(launch_files_app));
  DCHECK(result);
}

void ChromeNewWindowClient::OpenDownloadsFolder() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  base::FilePath target_directory =
      file_manager::util::GetDownloadsFolderForProfile(profile);
  if (OpenFilesSwa(profile, target_directory)) {
    return;
  }

  apps::AppServiceProxy* proxy =
      apps::AppServiceProxyFactory::GetForProfile(profile);
  auto downloads_path =
      file_manager::util::GetDownloadsFolderForProfile(profile);
  DCHECK(proxy);

  auto launch_files_app = [proxy,
                           downloads_path](const apps::AppUpdate& update) {
    if (update.Readiness() != apps::Readiness::kReady) {
      LOG(WARNING)
          << "Couldn't launch Files app because it isn't ready, readiness: "
          << static_cast<int>(update.Readiness());
      return;
    }

    std::vector<base::FilePath> launch_files;
    launch_files.push_back(downloads_path);
    proxy->LaunchAppWithFiles(
        update.AppId(),
        apps::GetEventFlags(WindowOpenDisposition::NEW_FOREGROUND_TAB,
                            /*prefer_container=*/true),
        apps::LaunchSource::kFromKeyboard, std::move(launch_files));
  };

  bool result = proxy->AppRegistryCache().ForOneApp(
      file_manager::kFileManagerAppId, launch_files_app);
  DCHECK(result);
}

void ChromeNewWindowClient::OpenCrosh() {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::CROSH);
}

void ChromeNewWindowClient::OpenGetHelp() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  chrome::ShowHelpForProfile(profile, chrome::HELP_SOURCE_KEYBOARD);
}

void ChromeNewWindowClient::RestoreTab() {
  if (tab_restore_helper_.get()) {
    DCHECK(!tab_restore_helper_->tab_restore_service()->IsLoaded());
    return;
  }

  Browser* browser = chrome::FindBrowserWithActiveWindow();
  Profile* profile = browser ? browser->profile() : nullptr;
  if (!profile)
    profile = ProfileManager::GetActiveUserProfile();
  if (profile->IsOffTheRecord())
    return;
  sessions::TabRestoreService* service =
      TabRestoreServiceFactory::GetForProfile(profile);
  if (!service)
    return;

  if (service->IsLoaded()) {
    RestoreTabUsingProfile(profile);
  } else {
    tab_restore_helper_ =
        std::make_unique<TabRestoreHelper>(this, profile, service);
    service->LoadTabsFromLastSession();
  }
}

void ChromeNewWindowClient::ShowShortcutCustomizationApp() {
  chrome::ShowShortcutCustomizationApp(ProfileManager::GetActiveUserProfile());
}

void ChromeNewWindowClient::ShowTaskManager() {
  chrome::OpenTaskManager(nullptr);
}

void ChromeNewWindowClient::OpenDiagnostics() {
  chrome::ShowDiagnosticsApp(ProfileManager::GetActiveUserProfile());
}

void ChromeNewWindowClient::OpenFeedbackPage(
    FeedbackSource source,
    const std::string& description_template) {
  chrome::OpenFeedbackDialog(chrome::FindBrowserWithActiveWindow(),
                             MapToChromeSource(source), description_template);
}

void ChromeNewWindowClient::OpenPersonalizationHub() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::PERSONALIZATION);
}

void ChromeNewWindowClient::OpenCaptivePortalSignin(const GURL& url) {
  chromeos::NetworkPortalSigninWindow::Get()->Show(url);
}

void ChromeNewWindowClient::OpenFile(const base::FilePath& file_path) {
  Profile* profile = ProfileManager::GetActiveUserProfile();
  platform_util::OpenItem(profile, file_path,
                          platform_util::OpenItemType::OPEN_FILE,
                          platform_util::OpenOperationCallback());
}

void ChromeNewWindowClient::LaunchCameraApp(const std::string& queries,
                                            int32_t task_id) {
  DCHECK(IsCameraAppEnabled());
  ChromeCameraAppUIDelegate::CameraAppDialog::ShowIntent(
      queries, arc::GetArcWindow(task_id));
  apps::RecordAppLaunch(web_app::kCameraAppId, apps::LaunchSource::kFromArc);
}

void ChromeNewWindowClient::CloseCameraApp() {
  const ash::ShelfID shelf_id(web_app::kCameraAppId);
  AppWindowShelfItemController* const app_controller =
      ChromeShelfController::instance()
          ->shelf_model()
          ->GetAppWindowShelfItemController(shelf_id);
  if (!app_controller)
    return;

  DCHECK_LE(app_controller->window_count(), 1lu);
  if (app_controller->window_count() > 0)
    app_controller->windows().front()->Close();
}

bool ChromeNewWindowClient::IsCameraAppEnabled() {
  Profile* const profile = ProfileManager::GetActiveUserProfile();
  auto* swa_manager = ash::SystemWebAppManager::Get(profile);
  return swa_manager &&
         swa_manager->IsAppEnabled(ash::SystemWebAppType::CAMERA);
}