chromium/chrome/browser/ash/app_list/app_service/app_service_context_menu.cc

// 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.

#include "chrome/browser/ash/app_list/app_service/app_service_context_menu.h"

#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.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/menu_util.h"
#include "chrome/browser/ash/app_list/app_context_menu_delegate.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/extension_app_utils.h"
#include "chrome/browser/ash/app_restore/full_restore_service.h"
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager_factory.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/shelf/standalone_browser_extension_app_context_menu.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/webui/ash/settings/app_management/app_management_uma.h"
#include "chrome/grit/generated_resources.h"
#include "components/app_constants/constants.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/context_menu_params.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/scoped_display_for_new_windows.h"
#include "ui/gfx/vector_icon_types.h"

namespace {
bool MenuItemHasLauncherContext(const extensions::MenuItem* item) {
  return item->contexts().Contains(extensions::MenuItem::LAUNCHER);
}

apps::WindowMode ConvertUseLaunchTypeCommandToWindowMode(int command_id) {
  DCHECK(command_id >= ash::USE_LAUNCH_TYPE_COMMAND_START &&
         command_id < ash::USE_LAUNCH_TYPE_COMMAND_END);
  switch (command_id) {
    case ash::USE_LAUNCH_TYPE_REGULAR:
      return apps::WindowMode::kBrowser;
    case ash::USE_LAUNCH_TYPE_WINDOW:
      return apps::WindowMode::kWindow;
    case ash::USE_LAUNCH_TYPE_TABBED_WINDOW:
      return apps::WindowMode::kTabbedWindow;
    case ash::DEPRECATED_USE_LAUNCH_TYPE_PINNED:
    case ash::DEPRECATED_USE_LAUNCH_TYPE_FULLSCREEN:
      [[fallthrough]];
    default:
      NOTREACHED_IN_MIGRATION();
      return apps::WindowMode::kUnknown;
  }
}

void CreateNewWindow(bool incognito, bool post_task) {
  if (post_task) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(CreateNewWindow, incognito, /*post_task=*/false));
    return;
  }

  ash::NewWindowDelegate::GetInstance()->NewWindow(
      incognito, /*should_trigger_session_restore=*/false);
}

void ShowOptionsPage(AppListControllerDelegate* controller,
                     Profile* profile,
                     const std::string& app_id,
                     bool post_task) {
  DCHECK(controller);
  DCHECK(profile);

  if (post_task) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(ShowOptionsPage, controller, profile, app_id,
                                  /*post_task=*/false));
    return;
  }

  controller->ShowOptionsPage(profile, app_id);
}

void ExecuteLaunchCommand(app_list::AppContextMenuDelegate* delegate,
                          int event_flags,
                          bool post_task) {
  DCHECK(delegate);
  if (post_task) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(ExecuteLaunchCommand, delegate, event_flags,
                                  /*post_task=*/false));
    return;
  }

  delegate->ExecuteLaunchCommand(event_flags);
}

}  // namespace

AppServiceContextMenu::AppServiceContextMenu(
    app_list::AppContextMenuDelegate* delegate,
    Profile* profile,
    const std::string& app_id,
    AppListControllerDelegate* controller,
    ash::AppListItemContext item_context)
    : AppContextMenu(delegate, profile, app_id, controller, item_context),
      proxy_(apps::AppServiceProxyFactory::GetForProfile(profile)) {
  proxy_->AppRegistryCache().ForOneApp(
      app_id, [this](const apps::AppUpdate& update) {
        app_type_ = apps_util::IsInstalled(update.Readiness())
                        ? update.AppType()
                        : apps::AppType::kUnknown;
        is_platform_app_ = update.IsPlatformApp().value_or(false);
      });
  // StandaloneBrowserExtension creates its own context menus for platform apps.
  if (app_type_ == apps::AppType::kStandaloneBrowserChromeApp &&
      is_platform_app_) {
    standalone_browser_extension_menu_ =
        std::make_unique<StandaloneBrowserExtensionAppContextMenu>(
            app_id, StandaloneBrowserExtensionAppContextMenu::Source::kAppList);
  }
}

AppServiceContextMenu::~AppServiceContextMenu() = default;

ui::ImageModel AppServiceContextMenu::GetIconForCommandId(
    int command_id) const {
  if (command_id == ash::LAUNCH_NEW) {
    const gfx::VectorIcon& icon =
        GetMenuItemVectorIcon(command_id, launch_new_string_id_);
    return ui::ImageModel::FromVectorIcon(
        icon, apps::GetColorIdForMenuItemIcon(), ash::kAppContextMenuIconSize);
  }
  return AppContextMenu::GetIconForCommandId(command_id);
}

std::u16string AppServiceContextMenu::GetLabelForCommandId(
    int command_id) const {
  if (command_id == ash::LAUNCH_NEW)
    return l10n_util::GetStringUTF16(launch_new_string_id_);

  return AppContextMenu::GetLabelForCommandId(command_id);
}

void AppServiceContextMenu::GetMenuModel(GetMenuModelCallback callback) {
  if (app_type_ == apps::AppType::kUnknown) {
    std::move(callback).Run(nullptr);
    return;
  }

  // StandaloneBrowserExtension handles its own context menus for platform apps.
  // Forward to that class.
  if (app_type_ == apps::AppType::kStandaloneBrowserChromeApp &&
      is_platform_app_) {
    standalone_browser_extension_menu_->GetMenuModel(std::move(callback));
    return;
  }

  proxy_->GetMenuModel(
      app_id(), apps::MenuType::kAppList, controller()->GetAppListDisplayId(),
      base::BindOnce(&AppServiceContextMenu::OnGetMenuModel,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void AppServiceContextMenu::ExecuteCommand(int command_id, int event_flags) {
  // StandaloneBrowserExtension handles its own context menus. Forward to that
  // class.
  if (standalone_browser_extension_menu_) {
    standalone_browser_extension_menu_->ExecuteCommand(command_id, event_flags);
    return;
  }

  // Place new windows on the same display as the context menu.
  display::ScopedDisplayForNewWindows scoped_display(
      controller()->GetAppListDisplayId());
  switch (command_id) {
    case ash::LAUNCH_NEW:
      ExecuteLaunchCommand(delegate(), event_flags, /*post_task=*/true);
      ash::full_restore::FullRestoreService::MaybeCloseNotification(profile());
      break;

    case ash::SHOW_APP_INFO:
      ShowAppInfo();
      ash::full_restore::FullRestoreService::MaybeCloseNotification(profile());
      break;

    case ash::OPTIONS:
      ShowOptionsPage(controller(), profile(), app_id(), /*post_task=*/true);
      ash::full_restore::FullRestoreService::MaybeCloseNotification(profile());
      break;

    case ash::UNINSTALL:
      controller()->UninstallApp(profile(), app_id());
      break;

    case ash::SETTINGS:
      if (app_id() == guest_os::kTerminalSystemAppId) {
        guest_os::LaunchTerminalSettings(profile(),
                                         controller()->GetAppListDisplayId());
        ash::full_restore::FullRestoreService::MaybeCloseNotification(
            profile());
      }
      break;

    case ash::APP_CONTEXT_MENU_NEW_WINDOW:
    case ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW: {
      const bool is_incognito =
          command_id == ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW;
      if (app_type_ == apps::AppType::kStandaloneBrowser) {
        crosapi::BrowserManager::Get()->NewWindow(
            is_incognito, /*should_trigger_session_restore=*/false);
      } else {
        // Create browser asynchronously to prevent this AppServiceContextMenu
        // object to be deleted when the browser window is shown.
        CreateNewWindow(is_incognito, /*post_task=*/true);
      }
      ash::full_restore::FullRestoreService::MaybeCloseNotification(profile());
      break;
    }
    case ash::SHUTDOWN_GUEST_OS:
      if (app_id() == guest_os::kTerminalSystemAppId) {
        crostini::CrostiniManager::GetForProfile(profile())->StopRunningVms(
            base::DoNothing());
      } else if (app_id() == plugin_vm::kPluginVmShelfAppId) {
        plugin_vm::PluginVmManagerFactory::GetForProfile(profile())
            ->StopPluginVm(plugin_vm::kPluginVmName, /*force=*/false);
      } else {
        LOG(ERROR) << "App " << app_id()
                   << " should not have a shutdown guest OS command.";
      }
      break;
    default:
      if (command_id >= ash::USE_LAUNCH_TYPE_COMMAND_START &&
          command_id < ash::USE_LAUNCH_TYPE_COMMAND_END) {
        launch_new_string_id_ =
            apps::StringIdForUseLaunchTypeCommand(command_id);

        if (app_type_ == apps::AppType::kWeb &&
            command_id == ash::USE_LAUNCH_TYPE_TABBED_WINDOW) {
          proxy_->SetWindowMode(app_id(), apps::WindowMode::kTabbedWindow);
          return;
        }

        SetLaunchType(command_id);
        return;
      }

      if (extensions::ContextMenuMatcher::IsExtensionsCustomCommandId(
              command_id)) {
        extension_menu_items_->ExecuteCommand(command_id, nullptr, nullptr,
                                              content::ContextMenuParams());
        return;
      }

      if (command_id >= ash::LAUNCH_APP_SHORTCUT_FIRST &&
          command_id <= ash::LAUNCH_APP_SHORTCUT_LAST) {
        ExecutePublisherContextMenuCommand(command_id);
        return;
      }

      AppContextMenu::ExecuteCommand(command_id, event_flags);
  }
}

bool AppServiceContextMenu::IsCommandIdChecked(int command_id) const {
  // StandaloneBrowserExtension handles its own context menus. Forward to that
  // class.
  if (standalone_browser_extension_menu_) {
    return standalone_browser_extension_menu_->IsCommandIdChecked(command_id);
  }

  switch (app_type_) {
    case apps::AppType::kWeb:
    case apps::AppType::kStandaloneBrowserChromeApp:  // hosted app
      if (command_id >= ash::USE_LAUNCH_TYPE_COMMAND_START &&
          command_id < ash::USE_LAUNCH_TYPE_COMMAND_END) {
        auto user_window_mode = apps::WindowMode::kUnknown;
        proxy_->AppRegistryCache().ForOneApp(
            app_id(), [&user_window_mode](const apps::AppUpdate& update) {
              user_window_mode = update.WindowMode();
            });
        return user_window_mode != apps::WindowMode::kUnknown &&
               user_window_mode ==
                   ConvertUseLaunchTypeCommandToWindowMode(command_id);
      }
      return AppContextMenu::IsCommandIdChecked(command_id);

    case apps::AppType::kChromeApp:
      if (command_id >= ash::USE_LAUNCH_TYPE_COMMAND_START &&
          command_id < ash::USE_LAUNCH_TYPE_COMMAND_END) {
        return static_cast<int>(
                   controller()->GetExtensionLaunchType(profile(), app_id())) +
                   ash::USE_LAUNCH_TYPE_COMMAND_START ==
               command_id;
      } else if (extensions::ContextMenuMatcher::IsExtensionsCustomCommandId(
                     command_id)) {
        return extension_menu_items_->IsCommandIdChecked(command_id);
      }
      return AppContextMenu::IsCommandIdChecked(command_id);

    case apps::AppType::kArc:
      [[fallthrough]];
    case apps::AppType::kCrostini:
      [[fallthrough]];
    case apps::AppType::kBuiltIn:
      [[fallthrough]];
    case apps::AppType::kPluginVm:
      [[fallthrough]];
    case apps::AppType::kBorealis:
      [[fallthrough]];
    default:
      return AppContextMenu::IsCommandIdChecked(command_id);
  }
}

bool AppServiceContextMenu::IsCommandIdEnabled(int command_id) const {
  // StandaloneBrowserExtension handles its own context menus. Forward to that
  // class.
  if (standalone_browser_extension_menu_) {
    return standalone_browser_extension_menu_->IsCommandIdEnabled(command_id);
  }

  if (extensions::ContextMenuMatcher::IsExtensionsCustomCommandId(command_id) &&
      extension_menu_items_) {
    return extension_menu_items_->IsCommandIdEnabled(command_id);
  }
  return AppContextMenu::IsCommandIdEnabled(command_id);
}

bool AppServiceContextMenu::IsItemForCommandIdDynamic(int command_id) const {
  return command_id == ash::LAUNCH_NEW ||
         AppContextMenu::IsItemForCommandIdDynamic(command_id);
}

void AppServiceContextMenu::OnGetMenuModel(GetMenuModelCallback callback,
                                           apps::MenuItems menu_items) {
  auto menu_model = std::make_unique<ui::SimpleMenuModel>(this);
  submenu_ = std::make_unique<ui::SimpleMenuModel>(this);
  size_t index = 0;

  if (!menu_items.items.empty() &&
      menu_items.items[0]->command_id == ash::LAUNCH_NEW) {
    apps::PopulateLaunchNewItemFromMenuItem(menu_items.items[0],
                                            menu_model.get(), submenu_.get(),
                                            &launch_new_string_id_);
    ++index;
  }

  // The special rule to ensure that FilesManager's first menu item is "New
  // window".
  const bool build_extension_menu_before_default =
      (app_type_ == apps::AppType::kChromeApp &&
       app_id() == extension_misc::kFilesManagerAppId);

  if (build_extension_menu_before_default)
    BuildExtensionAppShortcutsMenu(menu_model.get());

  // Create default items for non-Remote apps.
  if (app_id() != app_constants::kChromeAppId &&
      app_id() != app_constants::kLacrosAppId &&
      app_type_ != apps::AppType::kUnknown &&
      app_type_ != apps::AppType::kRemote) {
    app_list::AppContextMenu::BuildMenu(menu_model.get());
  }

  if (!build_extension_menu_before_default)
    BuildExtensionAppShortcutsMenu(menu_model.get());

  app_shortcut_items_ = std::make_unique<apps::AppShortcutItems>();
  for (size_t i = index; i < menu_items.items.size(); i++) {
    if (menu_items.items[i]->command_id == ash::LAUNCH_NEW) {
      // Crostini apps have `LAUNCH_NEW` menu item at non-0 position.
      apps::PopulateLaunchNewItemFromMenuItem(menu_items.items[i],
                                              menu_model.get(), submenu_.get(),
                                              &launch_new_string_id_);
    } else if (menu_items.items[i]->type == apps::MenuItemType::kCommand) {
      AddContextMenuOption(
          menu_model.get(),
          static_cast<ash::CommandId>(menu_items.items[i]->command_id),
          menu_items.items[i]->string_id);
    } else {
      apps::PopulateItemFromMenuItem(menu_items.items[i], menu_model.get(),
                                     app_shortcut_items_.get());
    }
  }

  AddReorderMenuOption(menu_model.get());

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

void AppServiceContextMenu::BuildExtensionAppShortcutsMenu(
    ui::SimpleMenuModel* menu_model) {
  extension_menu_items_ = std::make_unique<extensions::ContextMenuMatcher>(
      profile(), this, menu_model,
      base::BindRepeating(MenuItemHasLauncherContext));

  // Assign unique IDs to commands added by the app itself.
  int index = ash::USE_LAUNCH_TYPE_COMMAND_END;
  extension_menu_items_->AppendExtensionItems(
      extensions::MenuItem::ExtensionKey(app_id()), std::u16string(), &index,
      false /*is_action_menu*/);

  const int appended_count = index - ash::USE_LAUNCH_TYPE_COMMAND_END;
  app_list::AddMenuItemIconsForSystemApps(
      app_id(), menu_model, menu_model->GetItemCount() - appended_count,
      appended_count);
}

void AppServiceContextMenu::ShowAppInfo() {
  if (app_type_ == apps::AppType::kArc) {
    chrome::ShowAppManagementPage(
        profile(), app_id(),
        ash::settings::AppManagementEntryPoint::kAppListContextMenuAppInfoArc);
    return;
  }

  controller()->DoShowAppInfoFlow(profile(), app_id());
}

void AppServiceContextMenu::SetLaunchType(int command_id) {
  switch (app_type_) {
    case apps::AppType::kWeb:
    case apps::AppType::kStandaloneBrowserChromeApp: {
      // Web apps and standalone browser hosted apps can only toggle between
      // kWindow and kBrowser.
      apps::WindowMode user_window_mode =
          ConvertUseLaunchTypeCommandToWindowMode(command_id);
      if (user_window_mode != apps::WindowMode::kUnknown) {
        proxy_->SetWindowMode(app_id(), user_window_mode);
      }
      return;
    }
    case apps::AppType::kChromeApp: {
      // Hosted apps can only toggle between LAUNCH_TYPE_WINDOW and
      // LAUNCH_TYPE_REGULAR.
      extensions::LaunchType launch_type =
          (controller()->GetExtensionLaunchType(profile(), app_id()) ==
           extensions::LAUNCH_TYPE_WINDOW)
              ? extensions::LAUNCH_TYPE_REGULAR
              : extensions::LAUNCH_TYPE_WINDOW;
      controller()->SetExtensionLaunchType(profile(), app_id(), launch_type);
      return;
    }
    case apps::AppType::kArc:
      [[fallthrough]];
    case apps::AppType::kCrostini:
      [[fallthrough]];
    case apps::AppType::kBuiltIn:
      [[fallthrough]];
    case apps::AppType::kPluginVm:
      [[fallthrough]];
    case apps::AppType::kBorealis:
      [[fallthrough]];
    default:
      return;
  }
}

void AppServiceContextMenu::ExecutePublisherContextMenuCommand(int command_id) {
  DCHECK(command_id >= ash::LAUNCH_APP_SHORTCUT_FIRST &&
         command_id <= ash::LAUNCH_APP_SHORTCUT_LAST);
  const size_t index = command_id - ash::LAUNCH_APP_SHORTCUT_FIRST;
  DCHECK(app_shortcut_items_);
  DCHECK_LT(index, app_shortcut_items_->size());

  proxy_->ExecuteContextMenuCommand(app_id(), command_id,
                                    app_shortcut_items_->at(index).shortcut_id,
                                    controller()->GetAppListDisplayId());
}