chromium/chrome/browser/ui/ash/shelf/standalone_browser_extension_app_context_menu.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/shelf/standalone_browser_extension_app_context_menu.h"

#include <memory>
#include <utility>

#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_types.h"
#include "base/task/sequenced_task_runner.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/extension_apps_utils.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_prefs.h"
#include "chrome/browser/ui/ash/shelf/standalone_browser_extension_app_shelf_item_controller.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/services/app_service/public/cpp/app_types.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/vector_icons.h"

namespace {

// Create an appropriately sized ImageModel for a menu item icon.
ui::ImageModel GetMenuItemIcon(const gfx::VectorIcon& icon) {
  return ui::ImageModel::FromVectorIcon(
      icon,
      /*color_id=*/ui::kColorAshSystemUIMenuIcon, ash::kAppContextMenuIconSize);
}

}  // namespace

StandaloneBrowserExtensionAppContextMenu::
    StandaloneBrowserExtensionAppContextMenu(const std::string& app_id,
                                             Source source)
    : app_id_(app_id), source_(source) {}
StandaloneBrowserExtensionAppContextMenu::
    ~StandaloneBrowserExtensionAppContextMenu() = default;

void StandaloneBrowserExtensionAppContextMenu::GetMenuModel(
    GetMenuModelCallback callback) {
  // Always invoke the callback asynchronously to avoid re-entrancy.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&StandaloneBrowserExtensionAppContextMenu::OnGetMenuModel,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void StandaloneBrowserExtensionAppContextMenu::ExecuteCommand(int command_id,
                                                              int event_flags) {
  ash::ShelfModel* model = ash::ShelfModel::Get();
  const int item_index = model->ItemIndexByAppID(app_id_);
  const bool item_in_shelf = item_index >= 0;

  switch (static_cast<ash::CommandId>(command_id)) {
    case ash::SWAP_WITH_NEXT:
      if (!item_in_shelf)
        return;
      model->Swap(item_index, /*with_next=*/true);
      return;
    case ash::SWAP_WITH_PREVIOUS:
      if (!item_in_shelf)
        return;
      model->Swap(item_index, /*with_next=*/false);
      return;
    case ash::TOGGLE_PIN:
      if (item_in_shelf && model->IsAppPinned(app_id_)) {
        UnpinAppWithIDFromShelf(app_id_);
      } else {
        PinAppWithIDToShelf(app_id_);
      }
      return;
    case ash::MENU_CLOSE: {
      // There can only be a single active ash profile when Lacros is running.
      apps::AppServiceProxy* proxy =
          apps::AppServiceProxyFactory::GetForProfile(
              ProfileManager::GetPrimaryUserProfile());
      proxy->StopApp(app_id_);
      return;
    }

    case ash::UNINSTALL: {
      apps::UninstallSource uninstall_source =
          (source_ == Source::kShelf) ? apps::UninstallSource::kShelf
                                      : apps::UninstallSource::kAppList;
      aura::Window* window =
          (source_ == Source::kShelf)
              ? AppListClientImpl::GetInstance()->GetAppListWindow()
              : nullptr;
      apps::AppServiceProxy* proxy =
          apps::AppServiceProxyFactory::GetForProfile(
              ProfileManager::GetPrimaryUserProfile());
      proxy->Uninstall(app_id_, uninstall_source, /*parent_window=*/window);
      return;
    }
    case ash::SHOW_APP_INFO: {
      ash::settings::AppManagementEntryPoint entry =
          (source_ == Source::kShelf) ? ash::settings::AppManagementEntryPoint::
                                            kShelfContextMenuAppInfoChromeApp
                                      : ash::settings::AppManagementEntryPoint::
                                            kAppListContextMenuAppInfoChromeApp;
      chrome::ShowAppManagementPage(
          ProfileManager::GetPrimaryUserProfile(),
          apps::GetEscapedAppId(app_id_,
                                apps::AppType::kStandaloneBrowserChromeApp),
          entry);
      return;
    }
    default:
      return;
  }
}

void StandaloneBrowserExtensionAppContextMenu::OnGetMenuModel(
    GetMenuModelCallback callback) {
  bool allow_uninstall = false;
  bool allow_app_info = false;
  apps::AppServiceProxy* proxy = apps::AppServiceProxyFactory::GetForProfile(
      ProfileManager::GetPrimaryUserProfile());
  proxy->AppRegistryCache().ForOneApp(
      app_id_,
      [&allow_app_info, &allow_uninstall](const apps::AppUpdate& update) {
        allow_app_info = update.ShowInManagement().value_or(false);
        allow_uninstall = update.AllowUninstall().value_or(false);
      });

  bool allow_pin_unpin =
      GetPinnableForAppID(app_id_, ProfileManager::GetPrimaryUserProfile()) ==
      AppListControllerDelegate::PIN_EDITABLE;

  auto menu_model = std::make_unique<ui::SimpleMenuModel>(this);

  ash::ShelfModel* model = ash::ShelfModel::Get();
  const int item_index = model->ItemIndexByAppID(app_id_);
  const bool item_in_shelf = item_index >= 0;

  // Only show commands to reorder shelf items when ChromeVox or SwitchAccess
  // are enabled and the item is in the shelf.
  ash::AccessibilityManager* manager = ash::AccessibilityManager::Get();
  bool show_swap = manager &&
                   (manager->IsSpokenFeedbackEnabled() ||
                    manager->IsSwitchAccessEnabled()) &&
                   item_in_shelf;
  if (show_swap) {
    if (model->CanSwap(item_index, /*with_next=*/true)) {
      menu_model->AddItemWithStringId(ash::SWAP_WITH_NEXT,
                                      IDS_SHELF_CONTEXT_MENU_SWAP_WITH_NEXT);
    }
    if (model->CanSwap(item_index, /*with_next=*/false)) {
      menu_model->AddItemWithStringId(
          ash::SWAP_WITH_PREVIOUS, IDS_SHELF_CONTEXT_MENU_SWAP_WITH_PREVIOUS);
    }
  }

  if (allow_pin_unpin) {
    // This context menu is used by both the shelf and app list. We choose to
    // use the app list IDS string since it's clearer.
    bool currently_pinned = item_in_shelf && model->IsAppPinned(app_id_);
    const gfx::VectorIcon& icon =
        currently_pinned ? views::kUnpinIcon : views::kPinIcon;
    int ids = currently_pinned ? IDS_APP_LIST_CONTEXT_MENU_UNPIN
                               : IDS_APP_LIST_CONTEXT_MENU_PIN;

    menu_model->AddItemWithStringIdAndIcon(ash::TOGGLE_PIN, ids,
                                           GetMenuItemIcon(icon));
  }

  // Add a close menu item if there is at least one open window.
  if (item_in_shelf) {
    ash::ShelfItemDelegate* existing_delegate =
        ash::ShelfModel::Get()->GetShelfItemDelegate(ash::ShelfID(app_id_));
    StandaloneBrowserExtensionAppShelfItemController* controller =
        static_cast<StandaloneBrowserExtensionAppShelfItemController*>(
            existing_delegate);
    if (controller->WindowCount() >= 1) {
      menu_model->AddItemWithStringIdAndIcon(
          ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE,
          GetMenuItemIcon(views::kCloseIcon));
    }
  }

  // Add an uninstall menu item.
  if (allow_uninstall) {
    menu_model->AddItemWithStringIdAndIcon(
        ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM,
        GetMenuItemIcon(views::kUninstallIcon));
  }

  // Add a show app info menu item.
  if (allow_app_info) {
    menu_model->AddItemWithStringIdAndIcon(ash::SHOW_APP_INFO,
                                           IDS_APP_CONTEXT_MENU_SHOW_INFO,
                                           GetMenuItemIcon(views::kInfoIcon));
  }

  // TODO(crbug.com/40188614): Custom chrome app context menu items.

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