chromium/chrome/browser/apps/app_service/publishers/standalone_browser_extension_apps.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/apps/app_service/publishers/standalone_browser_extension_apps.h"

#include <utility>

#include "ash/public/cpp/app_menu_constants.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.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/menu_util.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_registry.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/features.h"
#include "components/app_restore/full_restore_utils.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/intent.h"

namespace apps {

namespace {

// Returns true if app's launch info should be saved to full restore.
bool ShouldSaveToFullRestore(AppServiceProxy* proxy,
                             const std::string& app_id) {
  if (!::full_restore::features::IsFullRestoreForLacrosEnabled()) {
    return false;
  }

  bool is_platform_app = true;
  proxy->AppRegistryCache().ForOneApp(
      app_id, [&is_platform_app](const apps::AppUpdate& update) {
        is_platform_app = update.IsPlatformApp().value_or(true);
      });
  // Note: Hosted apps will be restored by Lacros session restoration. No need
  // to save launch info to full restore. Only platform app's launch info should
  // be saved to full restore.
  return is_platform_app;
}

}  // namespace

StandaloneBrowserExtensionApps::StandaloneBrowserExtensionApps(
    AppServiceProxy* proxy,
    AppType app_type)
    : apps::AppPublisher(proxy), app_type_(app_type) {
  login_observation_.Observe(ash::LoginState::Get());
  // Check now in case login has already happened.
  LoggedInStateChanged();
}

StandaloneBrowserExtensionApps::~StandaloneBrowserExtensionApps() = default;

void StandaloneBrowserExtensionApps::RegisterCrosapiHost(
    mojo::PendingReceiver<crosapi::mojom::AppPublisher> receiver) {
  // At the moment the app service publisher will only accept one browser client
  // publishing apps to ash chrome. Any extra clients will be ignored.
  // TODO(crbug.com/40167449): Support SxS lacros.
  if (receiver_.is_bound()) {
    return;
  }
  receiver_.Bind(std::move(receiver));
  receiver_.set_disconnect_handler(
      base::BindOnce(&StandaloneBrowserExtensionApps::OnReceiverDisconnected,
                     weak_factory_.GetWeakPtr()));
}

void StandaloneBrowserExtensionApps::GetCompressedIconData(
    const std::string& app_id,
    int32_t size_in_dip,
    ui::ResourceScaleFactor scale_factor,
    LoadIconCallback callback) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    std::move(callback).Run(std::make_unique<IconValue>());
    return;
  }

  controller_->GetCompressedIcon(app_id, size_in_dip, scale_factor,
                                 std::move(callback));
}

void StandaloneBrowserExtensionApps::Launch(const std::string& app_id,
                                            int32_t event_flags,
                                            LaunchSource launch_source,
                                            WindowInfoPtr window_info) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  // The following code assumes |app_type_| must be
  // AppType::kStandaloneBrowserChromeApp. Therefore, the app must be either
  // platform app or hosted app.
  // In the future, this class is possible to be instantiated with other
  // AppType, please make sure to modify the logic if necessary.
  controller_->Launch(
      CreateCrosapiLaunchParamsWithEventFlags(
          proxy(), app_id, event_flags, launch_source,
          window_info ? window_info->display_id : display::kInvalidDisplayId),
      /*callback=*/base::DoNothing());

  if (ShouldSaveToFullRestore(proxy(), app_id)) {
    auto launch_info = std::make_unique<app_restore::AppLaunchInfo>(
        app_id, apps::LaunchContainer::kLaunchContainerNone,
        WindowOpenDisposition::UNKNOWN, display::kInvalidDisplayId,
        std::vector<base::FilePath>{}, nullptr);
    full_restore::SaveAppLaunchInfo(proxy()->profile()->GetPath(),
                                    std::move(launch_info));
  }
}

void StandaloneBrowserExtensionApps::LaunchAppWithFiles(
    const std::string& app_id,
    int32_t event_flags,
    LaunchSource launch_source,
    std::vector<base::FilePath> file_paths) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  std::vector<base::FilePath> file_paths_for_restore = file_paths;
  auto launch_params = crosapi::mojom::LaunchParams::New();
  launch_params->app_id = app_id;
  launch_params->launch_source = launch_source;
  launch_params->intent =
      apps_util::CreateCrosapiIntentForViewFiles(std::move(file_paths));
  controller_->Launch(std::move(launch_params),
                      /*callback=*/base::DoNothing());

  if (ShouldSaveToFullRestore(proxy(), app_id)) {
    auto launch_info = std::make_unique<app_restore::AppLaunchInfo>(
        app_id, apps::LaunchContainer::kLaunchContainerNone,
        WindowOpenDisposition::UNKNOWN, display::kInvalidDisplayId,
        std::move(file_paths_for_restore), nullptr);
    full_restore::SaveAppLaunchInfo(proxy()->profile()->GetPath(),
                                    std::move(launch_info));
  }
}

void StandaloneBrowserExtensionApps::LaunchAppWithIntent(
    const std::string& app_id,
    int32_t event_flags,
    IntentPtr intent,
    LaunchSource launch_source,
    WindowInfoPtr window_info,
    LaunchCallback callback) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    std::move(callback).Run(LaunchResult(State::kFailed));
    return;
  }

  auto launch_params = crosapi::mojom::LaunchParams::New();
  launch_params->app_id = app_id;
  launch_params->launch_source = launch_source;
  launch_params->intent = apps_util::ConvertAppServiceToCrosapiIntent(
      intent, ProfileManager::GetPrimaryUserProfile());
  controller_->Launch(std::move(launch_params),
                      /*callback=*/base::DoNothing());
  std::move(callback).Run(LaunchResult(State::kSuccess));

  if (ShouldSaveToFullRestore(proxy(), app_id)) {
    auto launch_info = std::make_unique<app_restore::AppLaunchInfo>(
        app_id, apps::LaunchContainer::kLaunchContainerNone,
        WindowOpenDisposition::UNKNOWN, display::kInvalidDisplayId,
        std::vector<base::FilePath>{}, std::move(intent));
    full_restore::SaveAppLaunchInfo(proxy()->profile()->GetPath(),
                                    std::move(launch_info));
  }
}

void StandaloneBrowserExtensionApps::LaunchAppWithParams(
    AppLaunchParams&& params,
    LaunchCallback callback) {
  if (!controller_.is_bound()) {
    std::move(callback).Run(LaunchResult());
    return;
  }

  controller_->Launch(
      apps::ConvertLaunchParamsToCrosapi(
          params, ProfileManager::GetPrimaryUserProfile()),
      apps::LaunchResultToMojomLaunchResultCallback(std::move(callback)));

  if (ShouldSaveToFullRestore(proxy(), params.app_id)) {
    auto launch_info = std::make_unique<app_restore::AppLaunchInfo>(
        params.app_id, params.container, params.disposition, params.display_id,
        std::move(params.launch_files), std::move(params.intent));
    full_restore::SaveAppLaunchInfo(proxy()->profile()->GetPath(),
                                    std::move(launch_info));
  }
}

void StandaloneBrowserExtensionApps::Uninstall(const std::string& app_id,
                                               UninstallSource uninstall_source,
                                               bool clear_site_data,
                                               bool report_abuse) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  controller_->Uninstall(app_id, uninstall_source, clear_site_data,
                         report_abuse);
}

void StandaloneBrowserExtensionApps::GetMenuModel(
    const std::string& app_id,
    MenuType menu_type,
    int64_t display_id,
    base::OnceCallback<void(MenuItems)> callback) {
  bool is_platform_app = true;
  bool can_use_uninstall = false;
  bool show_app_info = false;
  WindowMode display_mode = WindowMode::kUnknown;
  proxy()->AppRegistryCache().ForOneApp(
      app_id, [&is_platform_app, &can_use_uninstall, &show_app_info,
               &display_mode](const apps::AppUpdate& update) {
        is_platform_app = update.IsPlatformApp().value_or(true);
        can_use_uninstall = update.AllowUninstall().value_or(false);
        show_app_info = update.ShowInManagement().value_or(false);
        display_mode = update.WindowMode();
      });

  // This provides the context menu for hosted app in standalone browser.
  // Note: The context menu for platform app in standalone browser is provided
  // by StandaloneBrowserExtensionAppContextMenu.
  DCHECK(!is_platform_app);

  MenuItems menu_items;
  CreateOpenNewSubmenu(display_mode == WindowMode::kWindow
                           ? IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW
                           : IDS_APP_LIST_CONTEXT_MENU_NEW_TAB,
                       menu_items);

  if (menu_type == MenuType::kShelf) {
    if (proxy()->BrowserAppInstanceRegistry()->IsAppRunning(app_id)) {
      AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, menu_items);
    }
  }

  if (can_use_uninstall) {
    AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, menu_items);
  }

  if (show_app_info) {
    AddCommandItem(ash::SHOW_APP_INFO, IDS_APP_CONTEXT_MENU_SHOW_INFO,
                   menu_items);
  }

  std::move(callback).Run(std::move(menu_items));
}

void StandaloneBrowserExtensionApps::SetWindowMode(const std::string& app_id,
                                                   WindowMode window_mode) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  controller_->SetWindowMode(app_id, window_mode);
}

void StandaloneBrowserExtensionApps::StopApp(const std::string& app_id) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  controller_->StopApp(app_id);
}

void StandaloneBrowserExtensionApps::UpdateAppSize(const std::string& app_id) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  controller_->UpdateAppSize(app_id);
}

void StandaloneBrowserExtensionApps::OpenNativeSettings(
    const std::string& app_id) {
  // It is possible that Lacros is briefly unavailable, for example if it shuts
  // down for an update.
  if (!controller_.is_bound()) {
    return;
  }

  controller_->OpenNativeSettings(app_id);
}

void StandaloneBrowserExtensionApps::OnApps(std::vector<AppPtr> deltas) {
  if (deltas.empty()) {
    return;
  }

  if (controller_.is_bound()) {
    apps::AppPublisher::Publish(std::move(deltas), app_type_,
                                should_notify_initialized_);
    should_notify_initialized_ = false;
  } else {
    // If `controller_` is not bound, add `deltas` to `app_cache_` to wait for
    // registering the crosapi controller to publish all deltas saved in
    // `app_cache_`.
    for (AppPtr& delta : deltas) {
      app_cache_[delta->app_id] = std::move(delta);
    }
  }
}

void StandaloneBrowserExtensionApps::RegisterAppController(
    mojo::PendingRemote<crosapi::mojom::AppController> controller) {
  if (controller_.is_bound()) {
    return;
  }

  controller_.Bind(std::move(controller));
  controller_.set_disconnect_handler(
      base::BindOnce(&StandaloneBrowserExtensionApps::OnControllerDisconnected,
                     weak_factory_.GetWeakPtr()));
  RegisterPublisher(app_type_);

  if (app_cache_.empty()) {
    // If there is no apps saved in `app_cache_`, still publish an empty app
    // list to initialize `app_type_`.
    apps::AppPublisher::Publish(std::vector<AppPtr>{}, app_type_,
                                should_notify_initialized_);
  } else {
    std::vector<AppPtr> deltas;
    for (auto& it : app_cache_) {
      deltas.push_back(std::move(it.second));
    }
    app_cache_.clear();
    apps::AppPublisher::Publish(std::move(deltas), app_type_,
                                should_notify_initialized_);
  }
  should_notify_initialized_ = false;
}

void StandaloneBrowserExtensionApps::OnCapabilityAccesses(
    std::vector<CapabilityAccessPtr> deltas) {
  proxy()->OnCapabilityAccesses(std::move(deltas));
}

void StandaloneBrowserExtensionApps::LoggedInStateChanged() {
  if (ash::LoginState::Get()->IsUserLoggedIn()) {
    if (!keep_alive_) {
      if (app_type_ == AppType::kStandaloneBrowserChromeApp) {
        keep_alive_ = crosapi::BrowserManager::Get()->KeepAlive(
            crosapi::BrowserManager::Feature::kChromeApps);
      } else if (app_type_ == AppType::kStandaloneBrowserExtension) {
        keep_alive_ = crosapi::BrowserManager::Get()->KeepAlive(
            crosapi::BrowserManager::Feature::kExtensions);
      }
    }
  } else {
    keep_alive_.reset();
  }
}

void StandaloneBrowserExtensionApps::OnReceiverDisconnected() {
  receiver_.reset();
  controller_.reset();
}

void StandaloneBrowserExtensionApps::OnControllerDisconnected() {
  receiver_.reset();
  controller_.reset();
}

}  // namespace apps