// Copyright 2018 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/keyboard/chrome_keyboard_web_contents.h"
#include <string>
#include <utility>
#include "ash/keyboard/ui/resources/keyboard_resource_util.h"
#include "ash/style/color_util.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_bounds_observer.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/common/api/virtual_keyboard_private.h"
#include "extensions/common/constants.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "third_party/blink/public/common/input/web_gesture_event.h"
#include "ui/accessibility/aura/aura_window_properties.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/rect.h"
namespace {
// Deletes itself when the associated WebContents is destroyed.
class ChromeKeyboardContentsDelegate : public content::WebContentsDelegate,
public content::WebContentsObserver {
public:
ChromeKeyboardContentsDelegate() = default;
ChromeKeyboardContentsDelegate(const ChromeKeyboardContentsDelegate&) =
delete;
ChromeKeyboardContentsDelegate& operator=(
const ChromeKeyboardContentsDelegate&) = delete;
~ChromeKeyboardContentsDelegate() override = default;
private:
// content::WebContentsDelegate:
content::WebContents* OpenURLFromTab(
content::WebContents* source,
const content::OpenURLParams& params,
base::OnceCallback<void(content::NavigationHandle&)>
navigation_handle_callback) override {
auto navigation_handle = source->GetController().LoadURLWithParams(
content::NavigationController::LoadURLParams(params));
if (navigation_handle_callback && navigation_handle) {
std::move(navigation_handle_callback).Run(*navigation_handle);
}
Observe(source);
return source;
}
bool CanDragEnter(content::WebContents* source,
const content::DropData& data,
blink::DragOperationsMask operations_allowed) override {
return false;
}
bool IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url) override {
return true;
}
void SetContentsBounds(content::WebContents* source,
const gfx::Rect& bounds) override {
VLOG(1) << "SetContentsBounds: " << bounds.ToString();
aura::Window* keyboard_window = source->GetNativeView();
// keyboard window must have been added to keyboard container window at this
// point. Otherwise, wrong keyboard bounds is used and may cause problem as
// described in https://crbug.com/367788.
DCHECK(keyboard_window->parent());
// keyboard window bounds may not set to |pos| after this call. If keyboard
// is in FULL_WIDTH mode, only the height of keyboard window will be
// changed.
keyboard_window->SetBounds(bounds);
}
// content::WebContentsDelegate:
void RequestMediaAccessPermission(
content::WebContents* web_contents,
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback) override {
const extensions::Extension* extension = nullptr;
GURL origin(request.security_origin);
if (origin.SchemeIs(extensions::kExtensionScheme)) {
const extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext());
extension = registry->enabled_extensions().GetByID(origin.host());
DCHECK(extension);
}
MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
web_contents, request, std::move(callback), extension);
}
bool PreHandleGestureEvent(content::WebContents* source,
const blink::WebGestureEvent& event) override {
switch (event.GetType()) {
// Scroll events are not suppressed because the menu to select IME should
// be scrollable.
case blink::WebInputEvent::Type::kGestureScrollBegin:
case blink::WebInputEvent::Type::kGestureScrollEnd:
case blink::WebInputEvent::Type::kGestureScrollUpdate:
case blink::WebInputEvent::Type::kGestureFlingStart:
case blink::WebInputEvent::Type::kGestureFlingCancel:
return false;
// Allow tap events to allow browser scrollbars to be draggable by touch.
case blink::WebInputEvent::Type::kGestureTapDown:
case blink::WebInputEvent::Type::kGestureTap:
case blink::WebInputEvent::Type::kGestureTapCancel:
return false;
default:
// Stop gesture events from being passed to renderer to suppress the
// context menu. https://crbug.com/685140
return true;
}
}
// content::WebContentsObserver:
void WebContentsDestroyed() override { delete this; }
};
} // namespace
ChromeKeyboardWebContents::ChromeKeyboardWebContents(
content::BrowserContext* context,
const GURL& url,
LoadCallback load_callback,
UnembedCallback unembed_callback)
: load_callback_(std::move(load_callback)),
unembed_callback_(std::move(unembed_callback)) {
VLOG(1) << "ChromeKeyboardWebContents: " << url;
DCHECK(context);
content::WebContents::CreateParams web_contents_params(
context, content::SiteInstance::CreateForURL(context, url));
// The WebContents is initially hidden and shown later on.
web_contents_params.initially_hidden = true;
web_contents_ = content::WebContents::Create(web_contents_params);
web_contents_->SetDelegate(new ChromeKeyboardContentsDelegate());
web_contents_->SetColorProviderSource(&color_provider_source_);
extensions::SetViewType(web_contents_.get(),
extensions::mojom::ViewType::kComponent);
Observe(web_contents_.get());
LoadContents(url);
aura::Window* keyboard_window = web_contents_->GetNativeView();
keyboard_window->set_owned_by_parent(false);
// Set the background to be transparent for custom keyboard window shape.
content::RenderWidgetHostView* view =
web_contents_->GetPrimaryMainFrame()->GetView();
view->SetBackgroundColor(SK_ColorTRANSPARENT);
view->GetNativeView()->SetTransparent(true);
// By default, layers in WebContents are clipped at the window bounds,
// but this causes the shadows to be clipped too, so clipping needs to
// be disabled.
keyboard_window->layer()->SetMasksToBounds(false);
keyboard_window->SetProperty(ui::kAXRoleOverride, ax::mojom::Role::kKeyboard);
keyboard_window->AddObserver(this);
window_bounds_observer_ =
std::make_unique<ChromeKeyboardBoundsObserver>(keyboard_window);
}
ChromeKeyboardWebContents::~ChromeKeyboardWebContents() {
window_bounds_observer_.reset();
if (web_contents_) {
web_contents_->ClosePage();
web_contents_->GetNativeView()->RemoveObserver(this);
web_contents_.reset();
}
}
void ChromeKeyboardWebContents::SetKeyboardUrl(const GURL& new_url) {
GURL old_url = web_contents_->GetURL();
if (old_url == new_url)
return;
if (old_url.DeprecatedGetOriginAsURL() !=
new_url.DeprecatedGetOriginAsURL()) {
// Sets keyboard window rectangle to 0 and closes the current page before
// navigating to a keyboard in a different extension. This keeps the UX the
// same as Android. Note we need to explicitly close the current page as it
// might try to resize the keyboard window in javascript on a resize event.
TRACE_EVENT0("vk", "ReloadKeyboardIfNeeded");
web_contents_->GetNativeView()->SetBounds(gfx::Rect());
web_contents_->ClosePage();
}
LoadContents(new_url);
}
void ChromeKeyboardWebContents::SetInitialContentsSize(const gfx::Size& size) {
if (!contents_size_.IsEmpty())
return;
gfx::Rect bounds = web_contents_->GetNativeView()->bounds();
bounds.set_size(size);
web_contents_->GetNativeView()->SetBounds(bounds);
}
void ChromeKeyboardWebContents::RenderFrameCreated(
content::RenderFrameHost* frame_host) {
if (!frame_host->IsInPrimaryMainFrame())
return;
content::HostZoomMap* zoom_map =
content::HostZoomMap::GetDefaultForBrowserContext(
frame_host->GetBrowserContext());
zoom_map->SetTemporaryZoomLevel(frame_host->GetGlobalId(), 0 /* level */);
}
void ChromeKeyboardWebContents::DidStopLoading() {
// TODO(crbug.com/40577582): Change this to a DCHECK when we change
// ReloadKeyboardIfNeeded to also have a callback.
if (!load_callback_.is_null())
std::move(load_callback_).Run();
}
void ChromeKeyboardWebContents::OnColorProviderChanged() {
if (!web_contents_)
return;
auto* browser_context = web_contents_->GetBrowserContext();
if (!browser_context)
return;
auto* router = extensions::EventRouter::Get(browser_context);
if (!router ||
!router->HasEventListener(extensions::api::virtual_keyboard_private::
OnColorProviderChanged::kEventName)) {
return;
}
auto event = std::make_unique<extensions::Event>(
extensions::events::VIRTUAL_KEYBOARD_PRIVATE_ON_COLOR_PROVIDER_CHANGED,
extensions::api::virtual_keyboard_private::OnColorProviderChanged::
kEventName,
base::Value::List(), browser_context);
router->BroadcastEvent(std::move(event));
}
void ChromeKeyboardWebContents::LoadContents(const GURL& url) {
TRACE_EVENT0("vk", "LoadContents");
content::OpenURLParams params(url, content::Referrer(),
WindowOpenDisposition::SINGLETON_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false);
web_contents_->OpenURL(params, /*navigation_handle_callback=*/{});
}
void ChromeKeyboardWebContents::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
VLOG(1) << "OnWindowBoundsChanged: " << new_bounds.ToString();
contents_size_ = new_bounds.size();
}