chromium/chrome/browser/ui/ash/desks/desks_templates_app_launch_handler.cc

// Copyright 2021 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/desks/desks_templates_app_launch_handler.h"

#include <string>

#include "ash/public/cpp/desk_template.h"
#include "ash/wm/desks/desks_controller.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.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/ash/app_restore/app_launch_handler.h"
#include "chrome/browser/ash/app_restore/app_restore_arc_task_handler.h"
#include "chrome/browser/ash/app_restore/arc_app_queue_restore_handler.h"
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/desks/chrome_desks_util.h"
#include "chrome/browser/ui/ash/desks/desks_client.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.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_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "components/app_constants/constants.h"
#include "components/app_restore/app_restore_data.h"
#include "components/app_restore/app_restore_utils.h"
#include "components/app_restore/desk_template_read_handler.h"
#include "components/app_restore/restore_data.h"
#include "components/app_restore/window_info.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/tab_groups/tab_group_info.h"
#include "extensions/common/extension.h"

namespace {

// Returns the browser app name if it's an app type browser. Returns an empty
// string otherwise.
std::string GetBrowserAppName(
    const std::unique_ptr<app_restore::AppRestoreData>& app_restore_data,
    const std::string& app_id) {
  const bool app_type_browser =
      app_restore_data->browser_extra_info.app_type_browser.value_or(false);
  if (!app_type_browser)
    return std::string();

  const std::optional<std::string>& maybe_app_name =
      app_restore_data->browser_extra_info.app_name;
  return maybe_app_name.has_value() && !maybe_app_name.value().empty()
             ? maybe_app_name.value()
             : app_id;
}
}  // namespace

DesksTemplatesAppLaunchHandler::DesksTemplatesAppLaunchHandler(Profile* profile)
    : ash::AppLaunchHandler(profile),
      read_handler_(app_restore::DeskTemplateReadHandler::Get()) {}

DesksTemplatesAppLaunchHandler::~DesksTemplatesAppLaunchHandler() {
  if (launch_id_) {
    read_handler_->ClearRestoreData(launch_id_);

    if (auto* arc_task_handler =
            ash::app_restore::AppRestoreArcTaskHandler::GetForProfile(
                profile())) {
      arc_task_handler->ClearDeskTemplateArcAppQueueRestoreHandler(launch_id_);
    }
  }
}

void DesksTemplatesAppLaunchHandler::LaunchTemplate(
    const ash::DeskTemplate& desk_template) {
  // Ensure that the handler isn't re-used.
  DCHECK_EQ(launch_id_, 0);
  launch_id_ = desk_template.launch_id();

  DCHECK(desk_template.desk_restore_data());
  auto restore_data = desk_template.desk_restore_data()->Clone();
  DCHECK(!restore_data->app_id_to_launch_list().empty());

  read_handler_->SetRestoreData(launch_id_, restore_data->Clone());
  set_restore_data(std::move(restore_data));

  // Launch the different types of apps. They can be done in any order.
  MaybeLaunchArcApps();
  MaybeLaunchLacrosBrowsers();
  LaunchApps();
  LaunchBrowsers();
}

bool DesksTemplatesAppLaunchHandler::ShouldLaunchSystemWebAppOrChromeApp(
    const std::string& app_id,
    const app_restore::RestoreData::LaunchList& launch_list) {
  // Find out if the app can have multiple instances. Apps that can have
  // multiple instances are:
  //   1) System web apps which can open multiple windows
  //   2) Non platform app type Chrome apps
  // TODO(crbug.com/1239089): Investigate if we can have a way to handle moving
  // single instance windows without all these heuristics.

  bool is_multi_instance_window = false;

  // Check the app registry cache to see if the app is a system web app.
  bool is_system_web_app = false;
  apps::AppServiceProxyFactory::GetForProfile(profile())
      ->AppRegistryCache()
      .ForOneApp(app_id, [&is_system_web_app](const apps::AppUpdate& update) {
        if (update.AppType() == apps::AppType::kWeb ||
            update.AppType() == apps::AppType::kSystemWeb) {
          is_system_web_app = true;
        }
      });

  // A SWA can handle multiple instances if it can open multiple windows.
  if (is_system_web_app) {
    std::optional<ash::SystemWebAppType> swa_type =
        ash::GetSystemWebAppTypeForAppId(profile(), app_id);
    if (swa_type.has_value()) {
      auto* swa_manager = ash::SystemWebAppManager::Get(profile());
      DCHECK(swa_manager);
      auto* system_app = swa_manager->GetSystemApp(*swa_type);
      DCHECK(system_app);
      is_multi_instance_window = system_app->ShouldShowNewWindowMenuOption();
    }
  } else {
    // Check the extensions registry to see if the app is a platform app. No
    // need to do this check if the app is a system web app.
    DCHECK(!is_multi_instance_window);
    const extensions::Extension* extension =
        GetExtensionForAppID(app_id, profile());
    is_multi_instance_window = extension && !extension->is_platform_app();
  }

  // Do not try sending an existing window to the active desk and launch a new
  // instance.
  if (is_multi_instance_window)
    return true;

  const bool should_launch =
      ash::DesksController::Get()->OnSingleInstanceAppLaunchingFromSavedDesk(
          app_id, launch_list);

  // Notify performance tracker that some tracked windows will be moving.
  if (!should_launch) {
    for (const auto& window : launch_list)
      NotifyMovedSingleInstanceApp(window.first);
  }

  return should_launch;
}

void DesksTemplatesAppLaunchHandler::OnExtensionLaunching(
    const std::string& app_id) {
  read_handler_->SetNextRestoreWindowIdForChromeApp(app_id);
}

base::WeakPtr<ash::AppLaunchHandler>
DesksTemplatesAppLaunchHandler::GetWeakPtrAppLaunchHandler() {
  return weak_ptr_factory_.GetWeakPtr();
}

void DesksTemplatesAppLaunchHandler::LaunchBrowsers() {
  DCHECK(restore_data());

  const auto& launch_list = restore_data()->app_id_to_launch_list();
  for (const auto& iter : launch_list) {
    const std::string& app_id = iter.first;
    if (app_id != app_constants::kChromeAppId) {
      continue;
    }
    for (const auto& window_iter : iter.second) {
      const std::unique_ptr<app_restore::AppRestoreData>& app_restore_data =
          window_iter.second;

      const app_restore::BrowserExtraInfo browser_extra_info =
          app_restore_data->browser_extra_info;
      const std::vector<GURL>& urls = browser_extra_info.urls;
      if (urls.empty()) {
        continue;
      }

      const gfx::Rect current_bounds =
          app_restore_data->window_info.current_bounds.value_or(gfx::Rect());
      const std::string app_name = GetBrowserAppName(app_restore_data, app_id);
      if (!app_name.empty() && !IsBrowserAppInstalled(app_name)) {
        continue;
      }

      Browser::CreateParams create_params =
          !app_name.empty()
              ? Browser::CreateParams::CreateForApp(app_name,
                                                    /*trusted_source=*/true,
                                                    current_bounds, profile(),
                                                    /*user_gesture=*/false)
              : Browser::CreateParams(Browser::TYPE_NORMAL, profile(),
                                      /*user_gesture=*/false);

      create_params.restore_id = window_iter.first;
      create_params.creation_source = Browser::CreationSource::kDeskTemplate;

      const std::optional<chromeos::WindowStateType>& window_state_type =
          app_restore_data->window_info.window_state_type;
      if (window_state_type) {
        create_params.initial_show_state =
            chromeos::ToWindowShowState(*window_state_type);
      }

      if (!current_bounds.IsEmpty())
        create_params.initial_bounds = current_bounds;

      Browser* browser = Browser::Create(create_params);

      std::optional<int32_t> active_tab_index =
          browser_extra_info.active_tab_index;
      for (size_t i = 0; i < urls.size(); i++) {
        chrome::AddTabAt(browser, urls[i], /*index=*/-1,
                         /*foreground=*/
                         (active_tab_index &&
                          base::checked_cast<int32_t>(i) == *active_tab_index));
      }

      if (!browser_extra_info.tab_group_infos.empty()) {
        chrome_desks_util::AttachTabGroupsToBrowserInstance(
            browser_extra_info.tab_group_infos, browser);
      }

      if (browser_extra_info.first_non_pinned_tab_index.has_value() &&
          browser_extra_info.first_non_pinned_tab_index.value() <=
              static_cast<int>(urls.size())) {
        chrome_desks_util::SetBrowserPinnedTabs(
            browser_extra_info.first_non_pinned_tab_index.value(), browser);
      }

      // We need to handle minimized windows separately since unlike other
      // window types, it's not shown.
      if (window_state_type &&
          *window_state_type == chromeos::WindowStateType::kMinimized) {
        browser->window()->Minimize();
        continue;
      }

      browser->window()->ShowInactive();
    }
  }
  restore_data()->RemoveApp(app_constants::kChromeAppId);
}

void DesksTemplatesAppLaunchHandler::MaybeLaunchArcApps() {
  apps::AppRegistryCache& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile())
          ->AppRegistryCache();

  const auto& app_id_to_launch_list = restore_data()->app_id_to_launch_list();

  // Add the ready ARC apps in `app_id_to_launch_list` to `app_ids`.
  std::set<std::string> app_ids;
  cache.ForEachApp(
      [&app_ids, &app_id_to_launch_list](const apps::AppUpdate& update) {
        if (update.Readiness() == apps::Readiness::kReady &&
            update.AppType() == apps::AppType::kArc &&
            base::Contains(app_id_to_launch_list, update.AppId())) {
          app_ids.insert(update.AppId());
        }
      });

  // For each ARC app, check and see if there is an existing instance. We will
  // move this instance over instead of launching a new one. Remove the app
  // from the restore data if it was successfully moved so that the ARC launch
  // handler does not try to launch it later.
  for (const std::string& app_id : app_ids) {
    auto it = app_id_to_launch_list.find(app_id);
    DCHECK(it != app_id_to_launch_list.end());
    if (!ash::DesksController::Get()->OnSingleInstanceAppLaunchingFromSavedDesk(
            app_id, it->second)) {
      for (auto& window : it->second) {
        NotifyMovedSingleInstanceApp(window.first);
      }
      restore_data()->RemoveApp(app_id);
    }
  }

  auto* arc_task_handler =
      ash::app_restore::AppRestoreArcTaskHandler::GetForProfile(profile());
  if (!arc_task_handler)
    return;

  if (auto* launch_handler =
          arc_task_handler->GetDeskTemplateArcAppQueueRestoreHandler(
              launch_id_)) {
    launch_handler->set_desk_template_launch_id(launch_id_);
    launch_handler->RestoreArcApps(this);
  }
}

void DesksTemplatesAppLaunchHandler::MaybeLaunchLacrosBrowsers() {
  DCHECK(restore_data());

  const auto& launch_list = restore_data()->app_id_to_launch_list();
  for (const auto& iter : launch_list) {
    const std::string& app_id = iter.first;
    if (app_id != app_constants::kLacrosAppId)
      continue;

    // Count the number of lacros windows ash intends to launch. Will be
    // checked at lacros side to see if anything is missing between ash and
    // lacros when restoring saved desk.
    // TODO(crbug.com/40910343): Remove after issue is root caused.
    int windows_count = 0;

    for (const auto& [restore_window_id, app_restore_data] : iter.second) {
      const app_restore::BrowserExtraInfo& browser_extra_info =
          app_restore_data->browser_extra_info;
      if (browser_extra_info.urls.empty()) {
        continue;
      }
      const std::string app_name = GetBrowserAppName(app_restore_data, app_id);
      if (!app_name.empty() && !IsBrowserAppInstalled(app_name)) {
        continue;
      }

      // TODO(crbug.com/40910343): Remove after issue is root caused.
      windows_count++;
      LOG(ERROR) << "window " << restore_window_id << " launched by Ash with "
                 << app_restore_data->browser_extra_info.urls.size() << " tabs";

      crosapi::BrowserManager::Get()->CreateBrowserWithRestoredData(
          browser_extra_info.urls,
          app_restore_data->window_info.current_bounds.value_or(gfx::Rect()),
          browser_extra_info.tab_group_infos,
          chromeos::ToWindowShowState(
              app_restore_data->window_info.window_state_type.value_or(
                  chromeos::WindowStateType::kDefault)),
          browser_extra_info.active_tab_index.value_or(0),
          // Values of 0 will be ignored, other type constraints are
          // enforced on the browser side.
          browser_extra_info.first_non_pinned_tab_index.value_or(0), app_name,
          restore_window_id, browser_extra_info.lacros_profile_id.value_or(0));
    }
    // TODO(crbug.com/40910343): Remove after issue is root caused.
    LOG(ERROR) << windows_count
               << " windows launched by Ash in total for this desk";
  }
  restore_data()->RemoveApp(app_constants::kLacrosAppId);
}

void DesksTemplatesAppLaunchHandler::RecordRestoredAppLaunch(
    apps::AppTypeName app_type_name) {
  // TODO: Add UMA Histogram.
  NOTIMPLEMENTED();
}

void DesksTemplatesAppLaunchHandler::NotifyMovedSingleInstanceApp(
    int32_t window_id) {
  DesksClient::Get()->NotifyMovedSingleInstanceApp(window_id);
}

bool DesksTemplatesAppLaunchHandler::IsBrowserAppInstalled(
    const std::string& app_name) {
  apps::AppRegistryCache& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile())
          ->AppRegistryCache();

  std::string app_id = app_restore::GetAppIdFromAppName(app_name);
  bool result = false;
  cache.ForOneApp(app_id, [&result](const apps::AppUpdate& update) {
    if (apps_util::IsInstalled(update.Readiness()) &&
        update.AppType() != apps::AppType::kUnknown) {
      result = true;
    }
  });
  return result;
}