chromium/chrome/browser/ui/webui/ash/emoji/emoji_ui.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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ui/webui/ash/emoji/emoji_ui.h"

#include <iostream>

#include "ash/ash_element_identifiers.h"
#include "ash/constants/ash_features.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/views/bubble/webui_bubble_dialog_view.h"
#include "chrome/browser/ui/webui/ash/emoji/bubble_utils.h"
#include "chrome/browser/ui/webui/ash/emoji/seal_utils.h"
#include "chrome/browser/ui/webui/sanitized_image_source.h"
#include "chrome/browser/ui/webui/top_chrome/webui_contents_wrapper.h"
#include "chrome/browser/ui/webui/webui_util.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/emoji_picker_resources.h"
#include "chrome/grit/emoji_picker_resources_map.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/seal_resources.h"
#include "chrome/grit/seal_resources_map.h"
#include "chromeos/ash/components/emoji/grit/emoji_map.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "ui/base/emoji/emoji_panel_helper.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/display/screen.h"
#include "ui/resources/grit/webui_resources.h"
#include "ui/views/view_class_properties.h"
#include "ui/webui/color_change_listener/color_change_handler.h"

namespace {
constexpr gfx::Size kExtensionWindowSize(420, 480);
constexpr int kPaddingAroundCursor = 8;

class EmojiBubbleDialogView : public WebUIBubbleDialogView {
  METADATA_HEADER(EmojiBubbleDialogView, WebUIBubbleDialogView)

 public:
  explicit EmojiBubbleDialogView(
      std::unique_ptr<WebUIContentsWrapper> contents_wrapper,
      gfx::Rect caret_bounds)
      : WebUIBubbleDialogView(nullptr, contents_wrapper->GetWeakPtr()),
        contents_wrapper_(std::move(contents_wrapper)),
        caret_bounds_(caret_bounds) {
    set_has_parent(false);
    set_corner_radius(20);
    SetProperty(views::kElementIdentifierKey, ash::kEmojiPickerElementId);
  }

  // WebUIBubbleDialogView:
  void ResizeDueToAutoResize(content::WebContents* source,
                             const gfx::Size& new_size) override {
    WebUIBubbleDialogView::ResizeDueToAutoResize(source, new_size);
    GetWidget()->SetBounds(ash::GetBubbleBoundsAroundCaret(
        caret_bounds_,
        -GetBubbleFrameView()->bubble_border()->GetInsets().ToOutsets(),
        new_size));
  }

 private:
  std::unique_ptr<WebUIContentsWrapper> contents_wrapper_;
  gfx::Rect caret_bounds_;
};

BEGIN_METADATA(EmojiBubbleDialogView)
END_METADATA

emoji_picker::mojom::Category ConvertCategoryEnum(
    ui::EmojiPickerCategory category) {
  switch (category) {
    default:
    case ui::EmojiPickerCategory::kEmojis:
      return emoji_picker::mojom::Category::kEmojis;
    case ui::EmojiPickerCategory::kSymbols:
      return emoji_picker::mojom::Category::kSymbols;
    case ui::EmojiPickerCategory::kEmoticons:
      return emoji_picker::mojom::Category::kEmoticons;
    case ui::EmojiPickerCategory::kGifs:
      return emoji_picker::mojom::Category::kGifs;
  }
}

}  // namespace

namespace ash {

EmojiUIConfig::EmojiUIConfig()
    : DefaultTopChromeWebUIConfig(content::kChromeUIScheme,
                                  chrome::kChromeUIEmojiPickerHost) {}

bool EmojiUIConfig::ShouldAutoResizeHost() {
  return true;
}

EmojiUI::EmojiUI(content::WebUI* web_ui)
    : TopChromeWebUIController(web_ui,
                               true /* Needed for webui browser tests */),
      initial_category_(emoji_picker::mojom::Category::kEmojis) {
  content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd(
      web_ui->GetWebContents()->GetBrowserContext(),
      chrome::kChromeUIEmojiPickerHost);

  // Add required resources.
  webui::SetupWebUIDataSource(
      source, base::make_span(kEmojiPickerResources, kEmojiPickerResourcesSize),
      IDR_EMOJI_PICKER_INDEX_HTML);
  source->AddResourcePaths(base::make_span(kEmoji, kEmojiSize));

  // Add seal extra resources.
  if (SealUtils::ShouldEnable()) {
    source->AddResourcePaths(
        base::make_span(kSealResources, kSealResourcesSize));
  }

  // Some web components defined in seal extra resources are based on lit; so
  // we override content security policy here to make them work.
  source->OverrideContentSecurityPolicy(
      network::mojom::CSPDirectiveName::TrustedTypes,
      "trusted-types goog#html parse-html-subset sanitize-inner-html "
      "static-types lit-html lottie-worker-script-loader webui-test-script "
      "webui-test-html print-preview-plugin-loader polymer-html-literal "
      "polymer-template-event-attribute-policy;");

  Profile* profile = Profile::FromWebUI(web_ui);
  content::URLDataSource::Add(profile,
                              std::make_unique<SanitizedImageSource>(profile));
}

EmojiUI::~EmojiUI() = default;

bool EmojiUI::ShouldShow(const ui::TextInputClient* input_client,
                         ui::EmojiPickerFocusBehavior focus_behavior) {
  switch (focus_behavior) {
    case ui::EmojiPickerFocusBehavior::kOnlyShowWhenFocused:
      return input_client != nullptr;
    case ui::EmojiPickerFocusBehavior::kAlwaysShow:
      return true;
  }
}

void EmojiUI::Show(ui::EmojiPickerCategory category,
                   ui::EmojiPickerFocusBehavior focus_behavior,
                   const std::string& initial_query) {
  if (display::Screen::GetScreen()->InTabletMode()) {
    ui::ShowTabletModeEmojiPanel();
    return;
  }

  ui::InputMethod* input_method =
      IMEBridge::Get()->GetInputContextHandler()->GetInputMethod();
  ui::TextInputClient* input_client =
      input_method ? input_method->GetTextInputClient() : nullptr;

  if (!ShouldShow(input_client, focus_behavior)) {
    return;
  }

  auto* profile = ProfileManager::GetActiveUserProfile();

  if (!profile) {
    return;
  }

  const bool incognito_mode =
      input_client ? !input_client->ShouldDoLearning() : false;
  gfx::Rect caret_bounds =
      input_client ? input_client->GetCaretBounds() : gfx::Rect();

  // In general, try to show emoji picker near the text field. Some text clients
  // (like docs) set the actual input field way off screen in y. Allow for
  // slightly negative y, these will tend to be handled by adjust_if_offscreen,
  // but that can't handle things way off the screen so clamp large negative
  // values to zero to ensure picker is on screen.
  // TODO(b/189041846): Change this to take into account screen size in a more
  // general way.
  if (caret_bounds.y() < -5000) {
    caret_bounds.set_y(0);
  }

  gfx::Size window_size = kExtensionWindowSize;
  // This rect is used for positioning the emoji picker. It anchors either top
  // right / bottom left of the emoji picker window depending on where the text
  // field is. 8px padding around cursor is applied so that the emoji picker
  // does not cramp existing text.
  auto anchor_rect =
      gfx::Rect(caret_bounds.x() + window_size.width(),
                caret_bounds.y() - kPaddingAroundCursor, 0,
                caret_bounds.height() + kPaddingAroundCursor * 2);

  // TODO(b/181703133): Refactor so that the webui_bubble_manager can be used
  // here to reduce code duplication.

  auto contents_wrapper = std::make_unique<WebUIContentsWrapperT<EmojiUI>>(
      GURL(chrome::kChromeUIEmojiPickerURL), profile, IDS_ACCNAME_EMOJI_PICKER);
  // Need to reload the web contents here because the view isn't visible unless
  // ShowUI is called from the JS side.  By reloading, we trigger the JS to
  // eventually call ShowUI().
  contents_wrapper->GetWebUIController()->incognito_mode_ = incognito_mode;
  contents_wrapper->GetWebUIController()->no_text_field_ =
      input_client == nullptr;
  contents_wrapper->GetWebUIController()->initial_category_ =
      ConvertCategoryEnum(category);
  contents_wrapper->GetWebUIController()->initial_query_ = initial_query;

  auto bubble_view = std::make_unique<EmojiBubbleDialogView>(
      std::move(contents_wrapper), caret_bounds);
  auto weak_ptr = bubble_view->GetWeakPtr();
  views::BubbleDialogDelegateView::CreateBubble(std::move(bubble_view));
}

WEB_UI_CONTROLLER_TYPE_IMPL(EmojiUI)

void EmojiUI::BindInterface(
    mojo::PendingReceiver<color_change_listener::mojom::PageHandler> receiver) {
  color_provider_handler_ = std::make_unique<ui::ColorChangeHandler>(
      web_ui()->GetWebContents(), std::move(receiver));
}

void EmojiUI::BindInterface(
    mojo::PendingReceiver<emoji_search::mojom::EmojiSearch> receiver) {
  emoji_search_ = std::make_unique<EmojiSearchProxy>(std::move(receiver));
}

void EmojiUI::BindInterface(
    mojo::PendingReceiver<emoji_picker::mojom::PageHandlerFactory> receiver) {
  page_factory_receiver_.reset();
  page_factory_receiver_.Bind(std::move(receiver));
}

void EmojiUI::BindInterface(
    mojo::PendingReceiver<new_window_proxy::mojom::NewWindowProxy> receiver) {
  new_window_proxy_ =
      std::make_unique<ash::NewWindowProxy>(std::move(receiver));
}

void EmojiUI::BindInterface(
    mojo::PendingReceiver<seal::mojom::SealService> receiver) {
  if (SealUtils::ShouldEnable()) {
    Profile* profile = Profile::FromWebUI(web_ui());
    manta::MantaService* manta_service =
        manta::MantaServiceFactory::GetForProfile(profile);
    seal_service_ = std::make_unique<SealService>(
        /*receiver=*/std::move(receiver),
        /*snapper_provider=*/manta_service->CreateSnapperProvider());
  }
}

void EmojiUI::CreatePageHandler(
    mojo::PendingReceiver<emoji_picker::mojom::PageHandler> receiver) {
  page_handler_ = std::make_unique<EmojiPageHandler>(
      std::move(receiver), web_ui(), this, incognito_mode_, no_text_field_,
      initial_category_, initial_query_);
}

}  // namespace ash