| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // 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/chrome_keyboard_ui.h" |
| |
| #include <set> |
| #include <string> |
| #include <utility> |
| |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "base/macros.h" |
| #include "base/no_destructor.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/chrome_extension_web_contents_observer.h" |
| #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" |
| #include "content/public/browser/host_zoom_map.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/render_widget_host_iterator.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 "content/public/browser/web_ui.h" |
| #include "content/public/common/bindings_policy.h" |
| #include "extensions/browser/api/virtual_keyboard_private/virtual_keyboard_delegate.h" |
| #include "extensions/browser/api/virtual_keyboard_private/virtual_keyboard_private_api.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/extension_messages.h" |
| #include "ipc/ipc_message_macros.h" |
| #include "ui/aura/layout_manager.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_event_dispatcher.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/ime/chromeos/input_method_manager.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/compositor_extra/shadow.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/keyboard/keyboard_controller.h" |
| #include "ui/keyboard/keyboard_controller_observer.h" |
| #include "ui/keyboard/keyboard_resource_util.h" |
| #include "ui/keyboard/keyboard_switches.h" |
| #include "ui/keyboard/keyboard_util.h" |
| #include "ui/wm/core/shadow_types.h" |
| |
| namespace virtual_keyboard_private = extensions::api::virtual_keyboard_private; |
| |
| namespace { |
| |
| GURL& GetOverrideVirtualKeyboardUrl() { |
| static base::NoDestructor<GURL> url; |
| return *url; |
| } |
| |
| class WindowBoundsChangeObserver : public aura::WindowObserver { |
| public: |
| explicit WindowBoundsChangeObserver(ChromeKeyboardUI* ui) : ui_(ui) {} |
| ~WindowBoundsChangeObserver() override {} |
| |
| void AddObservedWindow(aura::Window* window) { |
| if (!window->HasObserver(this)) { |
| window->AddObserver(this); |
| observed_windows_.insert(window); |
| } |
| } |
| void RemoveAllObservedWindows() { |
| for (aura::Window* window : observed_windows_) |
| window->RemoveObserver(this); |
| observed_windows_.clear(); |
| } |
| |
| private: |
| void OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) override { |
| ui_->UpdateInsetsForWindow(window); |
| } |
| void OnWindowDestroyed(aura::Window* window) override { |
| if (window->HasObserver(this)) |
| window->RemoveObserver(this); |
| observed_windows_.erase(window); |
| } |
| |
| ChromeKeyboardUI* const ui_; |
| std::set<aura::Window*> observed_windows_; |
| |
| DISALLOW_COPY_AND_ASSIGN(WindowBoundsChangeObserver); |
| }; |
| |
| // The WebContentsDelegate for the chrome keyboard. |
| // The delegate deletes itself when the keyboard is destroyed. |
| class ChromeKeyboardContentsDelegate : public content::WebContentsDelegate, |
| public content::WebContentsObserver { |
| public: |
| explicit ChromeKeyboardContentsDelegate(ChromeKeyboardUI* ui) : ui_(ui) {} |
| ~ChromeKeyboardContentsDelegate() override {} |
| |
| private: |
| // content::WebContentsDelegate: |
| content::WebContents* OpenURLFromTab( |
| content::WebContents* source, |
| const content::OpenURLParams& params) override { |
| source->GetController().LoadURL(params.url, params.referrer, |
| params.transition, params.extra_headers); |
| Observe(source); |
| return source; |
| } |
| |
| bool CanDragEnter(content::WebContents* source, |
| const content::DropData& data, |
| blink::WebDragOperationsMask operations_allowed) override { |
| return false; |
| } |
| |
| bool ShouldCreateWebContents( |
| content::WebContents* web_contents, |
| content::RenderFrameHost* opener, |
| content::SiteInstance* source_site_instance, |
| int32_t route_id, |
| int32_t main_frame_route_id, |
| int32_t main_frame_widget_route_id, |
| content::mojom::WindowContainerType window_container_type, |
| const GURL& opener_url, |
| const std::string& frame_name, |
| const GURL& target_url, |
| const std::string& partition_id, |
| content::SessionStorageNamespace* session_storage_namespace) override { |
| return false; |
| } |
| |
| void SetContentsBounds(content::WebContents* source, |
| const gfx::Rect& bounds) override { |
| aura::Window* keyboard = ui_->GetContentsWindow(); |
| // 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 crbug.com/367788. |
| DCHECK(keyboard->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->SetBounds(bounds); |
| } |
| |
| // content::WebContentsDelegate: |
| void RequestMediaAccessPermission( |
| content::WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback) override { |
| ui_->RequestAudioInput(web_contents, request, std::move(callback)); |
| } |
| |
| // content::WebContentsDelegate: |
| 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::kGestureScrollBegin: |
| case blink::WebInputEvent::kGestureScrollEnd: |
| case blink::WebInputEvent::kGestureScrollUpdate: |
| case blink::WebInputEvent::kGestureFlingStart: |
| case blink::WebInputEvent::kGestureFlingCancel: |
| return false; |
| default: |
| // Stop gesture events from being passed to renderer to suppress the |
| // context menu. crbug.com/685140 |
| return true; |
| } |
| } |
| |
| // content::WebContentsObserver: |
| void WebContentsDestroyed() override { delete this; } |
| |
| ChromeKeyboardUI* const ui_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ChromeKeyboardContentsDelegate); |
| }; |
| |
| class AshKeyboardControllerObserver |
| : public keyboard::KeyboardControllerObserver { |
| public: |
| explicit AshKeyboardControllerObserver(content::BrowserContext* context) |
| : context_(context) {} |
| ~AshKeyboardControllerObserver() override {} |
| |
| // KeyboardControllerObserver: |
| void OnKeyboardVisibleBoundsChanged(const gfx::Rect& bounds) override { |
| extensions::EventRouter* router = extensions::EventRouter::Get(context_); |
| |
| if (!router->HasEventListener( |
| virtual_keyboard_private::OnBoundsChanged::kEventName)) { |
| return; |
| } |
| |
| auto event_args = std::make_unique<base::ListValue>(); |
| auto new_bounds = std::make_unique<base::DictionaryValue>(); |
| new_bounds->SetInteger("left", bounds.x()); |
| new_bounds->SetInteger("top", bounds.y()); |
| new_bounds->SetInteger("width", bounds.width()); |
| new_bounds->SetInteger("height", bounds.height()); |
| event_args->Append(std::move(new_bounds)); |
| |
| auto event = std::make_unique<extensions::Event>( |
| extensions::events::VIRTUAL_KEYBOARD_PRIVATE_ON_BOUNDS_CHANGED, |
| virtual_keyboard_private::OnBoundsChanged::kEventName, |
| std::move(event_args), context_); |
| router->BroadcastEvent(std::move(event)); |
| } |
| |
| void OnKeyboardClosed() override { |
| extensions::EventRouter* router = extensions::EventRouter::Get(context_); |
| |
| if (!router->HasEventListener( |
| virtual_keyboard_private::OnKeyboardClosed::kEventName)) { |
| return; |
| } |
| |
| auto event = std::make_unique<extensions::Event>( |
| extensions::events::VIRTUAL_KEYBOARD_PRIVATE_ON_KEYBOARD_CLOSED, |
| virtual_keyboard_private::OnKeyboardClosed::kEventName, |
| std::make_unique<base::ListValue>(), context_); |
| router->BroadcastEvent(std::move(event)); |
| } |
| |
| void OnKeyboardConfigChanged() override { |
| extensions::VirtualKeyboardAPI* api = |
| extensions::BrowserContextKeyedAPIFactory< |
| extensions::VirtualKeyboardAPI>::Get(context_); |
| api->delegate()->OnKeyboardConfigChanged(); |
| } |
| |
| private: |
| content::BrowserContext* const context_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AshKeyboardControllerObserver); |
| }; |
| |
| } // namespace |
| |
| void ChromeKeyboardUI::TestApi::SetOverrideVirtualKeyboardUrl(const GURL& url) { |
| GURL& override_url = GetOverrideVirtualKeyboardUrl(); |
| override_url = url; |
| } |
| |
| ChromeKeyboardUI::ChromeKeyboardUI(content::BrowserContext* context) |
| : browser_context_(context), |
| default_url_(keyboard::kKeyboardURL), |
| window_bounds_observer_( |
| std::make_unique<WindowBoundsChangeObserver>(this)) {} |
| |
| ChromeKeyboardUI::~ChromeKeyboardUI() { |
| ResetInsets(); |
| DCHECK(!keyboard_controller()); |
| } |
| |
| void ChromeKeyboardUI::RequestAudioInput( |
| content::WebContents* web_contents, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback) { |
| const extensions::Extension* extension = nullptr; |
| GURL origin(request.security_origin); |
| if (origin.SchemeIs(extensions::kExtensionScheme)) { |
| const extensions::ExtensionRegistry* registry = |
| extensions::ExtensionRegistry::Get(browser_context()); |
| extension = registry->enabled_extensions().GetByID(origin.host()); |
| DCHECK(extension); |
| } |
| |
| MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest( |
| web_contents, request, std::move(callback), extension); |
| } |
| |
| void ChromeKeyboardUI::UpdateInsetsForWindow(aura::Window* window) { |
| aura::Window* keyboard_container = |
| keyboard_controller()->GetContainerWindow(); |
| if (!ShouldWindowOverscroll(window)) |
| return; |
| |
| std::unique_ptr<content::RenderWidgetHostIterator> widgets( |
| content::RenderWidgetHost::GetRenderWidgetHosts()); |
| while (content::RenderWidgetHost* widget = widgets->GetNextHost()) { |
| content::RenderWidgetHostView* view = widget->GetView(); |
| if (view && window->Contains(view->GetNativeView())) { |
| gfx::Rect window_bounds = view->GetNativeView()->GetBoundsInScreen(); |
| gfx::Rect intersect = |
| gfx::IntersectRects(window_bounds, keyboard_container->bounds()); |
| int overlap = ShouldEnableInsets(window) ? intersect.height() : 0; |
| if (overlap > 0 && overlap < window_bounds.height()) |
| view->SetInsets(gfx::Insets(0, 0, overlap, 0)); |
| else |
| view->SetInsets(gfx::Insets()); |
| return; |
| } |
| } |
| } |
| |
| aura::Window* ChromeKeyboardUI::GetContentsWindow() { |
| if (!keyboard_contents_) { |
| keyboard_contents_ = CreateWebContents(); |
| keyboard_contents_->SetDelegate(new ChromeKeyboardContentsDelegate(this)); |
| SetupWebContents(keyboard_contents_.get()); |
| LoadContents(GetVirtualKeyboardUrl()); |
| keyboard_contents_->GetNativeView()->AddObserver(this); |
| content::RenderWidgetHostView* view = |
| keyboard_contents_->GetMainFrame()->GetView(); |
| view->SetBackgroundColor(SK_ColorTRANSPARENT); |
| view->GetNativeView()->SetTransparent(true); |
| } |
| |
| return keyboard_contents_->GetNativeView(); |
| } |
| |
| bool ChromeKeyboardUI::HasContentsWindow() const { |
| return !!keyboard_contents_; |
| } |
| |
| bool ChromeKeyboardUI::ShouldWindowOverscroll(aura::Window* window) const { |
| aura::Window* root_window = window->GetRootWindow(); |
| if (!root_window) |
| return true; |
| |
| if (root_window != GetKeyboardRootWindow()) |
| return false; |
| |
| ash::RootWindowController* root_window_controller = |
| ash::RootWindowController::ForWindow(root_window); |
| // Shell ime window container contains virtual keyboard windows and IME |
| // windows(IME windows are created by chrome.app.window.create api). They |
| // should never be overscrolled. |
| return !root_window_controller |
| ->GetContainer(ash::kShellWindowId_ImeWindowParentContainer) |
| ->Contains(window); |
| } |
| |
| void ChromeKeyboardUI::ReloadKeyboardIfNeeded() { |
| DCHECK(keyboard_contents_); |
| if (keyboard_contents_->GetURL() != GetVirtualKeyboardUrl()) { |
| if (keyboard_contents_->GetURL().GetOrigin() != |
| GetVirtualKeyboardUrl().GetOrigin()) { |
| // Sets keyboard window rectangle to 0 and close current page before |
| // navigate to a keyboard in a different extension. This keeps the UX the |
| // same as Android. Note we need to explicitly close current page as it |
| // might try to resize keyboard window in javascript on a resize event. |
| TRACE_EVENT0("vk", "ReloadKeyboardIfNeeded"); |
| GetContentsWindow()->SetBounds(gfx::Rect()); |
| keyboard_contents_->ClosePage(); |
| } |
| LoadContents(GetVirtualKeyboardUrl()); |
| } |
| } |
| |
| void ChromeKeyboardUI::InitInsets(const gfx::Rect& new_bounds) { |
| // Adjust the height of the viewport for visible windows on the primary |
| // display. |
| // TODO(kevers): Add EnvObserver to properly initialize insets if a |
| // window is created while the keyboard is visible. |
| std::unique_ptr<content::RenderWidgetHostIterator> widgets( |
| content::RenderWidgetHost::GetRenderWidgetHosts()); |
| while (content::RenderWidgetHost* widget = widgets->GetNextHost()) { |
| content::RenderWidgetHostView* view = widget->GetView(); |
| // Can be NULL, e.g. if the RenderWidget is being destroyed or |
| // the render process crashed. |
| if (view) { |
| aura::Window* window = view->GetNativeView(); |
| // Added while we determine if RenderWidgetHostViewChildFrame can be |
| // changed to always return a non-null value: https://crbug.com/644726 . |
| // If we cannot guarantee a non-null value, then this may need to stay. |
| if (!window) |
| continue; |
| |
| if (ShouldWindowOverscroll(window)) { |
| gfx::Rect window_bounds = window->GetBoundsInScreen(); |
| gfx::Rect intersect = gfx::IntersectRects(window_bounds, new_bounds); |
| int overlap = intersect.height(); |
| |
| // TODO(crbug.com/826617): get the actual obscured height from IME side. |
| if (keyboard::IsFullscreenHandwritingVirtualKeyboardEnabled()) |
| overlap = 0; |
| |
| if (overlap > 0 && overlap < window_bounds.height()) |
| view->SetInsets(gfx::Insets(0, 0, overlap, 0)); |
| else |
| view->SetInsets(gfx::Insets()); |
| AddBoundsChangedObserver(window); |
| } |
| } |
| } |
| } |
| |
| void ChromeKeyboardUI::ResetInsets() { |
| const gfx::Insets insets; |
| std::unique_ptr<content::RenderWidgetHostIterator> widgets( |
| content::RenderWidgetHost::GetRenderWidgetHosts()); |
| while (content::RenderWidgetHost* widget = widgets->GetNextHost()) { |
| content::RenderWidgetHostView* view = widget->GetView(); |
| if (view) |
| view->SetInsets(insets); |
| } |
| window_bounds_observer_->RemoveAllObservedWindows(); |
| } |
| |
| void ChromeKeyboardUI::OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| SetShadowAroundKeyboard(); |
| } |
| |
| void ChromeKeyboardUI::OnWindowDestroyed(aura::Window* window) { |
| window->RemoveObserver(this); |
| } |
| |
| void ChromeKeyboardUI::OnWindowParentChanged(aura::Window* window, |
| aura::Window* parent) { |
| SetShadowAroundKeyboard(); |
| } |
| |
| const aura::Window* ChromeKeyboardUI::GetKeyboardRootWindow() const { |
| return keyboard_contents_ |
| ? keyboard_contents_->GetNativeView()->GetRootWindow() |
| : nullptr; |
| } |
| |
| std::unique_ptr<content::WebContents> ChromeKeyboardUI::CreateWebContents() { |
| content::BrowserContext* context = browser_context(); |
| return content::WebContents::Create(content::WebContents::CreateParams( |
| context, |
| content::SiteInstance::CreateForURL(context, GetVirtualKeyboardUrl()))); |
| } |
| |
| void ChromeKeyboardUI::LoadContents(const GURL& url) { |
| if (keyboard_contents_) { |
| TRACE_EVENT0("vk", "LoadContents"); |
| content::OpenURLParams params(url, content::Referrer(), |
| WindowOpenDisposition::SINGLETON_TAB, |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false); |
| keyboard_contents_->OpenURL(params); |
| } |
| } |
| |
| const GURL& ChromeKeyboardUI::GetVirtualKeyboardUrl() { |
| const GURL& override_url = GetOverrideVirtualKeyboardUrl(); |
| if (!override_url.is_empty()) |
| return override_url; |
| |
| chromeos::input_method::InputMethodManager* ime_manager = |
| chromeos::input_method::InputMethodManager::Get(); |
| if (!keyboard::IsInputViewEnabled() || !ime_manager || |
| !ime_manager->GetActiveIMEState()) |
| return default_url_; |
| |
| const GURL& input_view_url = |
| ime_manager->GetActiveIMEState()->GetInputViewUrl(); |
| return input_view_url.is_valid() ? input_view_url : default_url_; |
| } |
| |
| bool ChromeKeyboardUI::ShouldEnableInsets(aura::Window* window) { |
| aura::Window* contents_window = GetContentsWindow(); |
| return (contents_window->GetRootWindow() == window->GetRootWindow() && |
| keyboard::IsKeyboardOverscrollEnabled() && |
| contents_window->IsVisible() && |
| keyboard_controller()->keyboard_visible() && |
| !keyboard::IsFullscreenHandwritingVirtualKeyboardEnabled()); |
| } |
| |
| void ChromeKeyboardUI::AddBoundsChangedObserver(aura::Window* window) { |
| aura::Window* target_window = window ? window->GetToplevelWindow() : nullptr; |
| if (target_window) |
| window_bounds_observer_->AddObservedWindow(target_window); |
| } |
| |
| void ChromeKeyboardUI::SetShadowAroundKeyboard() { |
| aura::Window* contents_window = keyboard_contents_->GetNativeView(); |
| if (!contents_window->parent()) |
| return; |
| |
| if (!shadow_) { |
| shadow_ = std::make_unique<ui::Shadow>(); |
| shadow_->Init(wm::kShadowElevationActiveWindow); |
| shadow_->layer()->SetVisible(true); |
| contents_window->parent()->layer()->Add(shadow_->layer()); |
| } |
| |
| shadow_->SetContentBounds(contents_window->bounds()); |
| } |
| |
| void ChromeKeyboardUI::SetupWebContents(content::WebContents* contents) { |
| extensions::SetViewType(contents, extensions::VIEW_TYPE_COMPONENT); |
| extensions::ChromeExtensionWebContentsObserver::CreateForWebContents( |
| contents); |
| Observe(contents); |
| } |
| |
| ui::InputMethod* ChromeKeyboardUI::GetInputMethod() { |
| aura::Window* root_window = ash::Shell::GetRootWindowForNewWindows(); |
| DCHECK(root_window); |
| return root_window->GetHost()->GetInputMethod(); |
| } |
| |
| void ChromeKeyboardUI::SetController(keyboard::KeyboardController* controller) { |
| // During KeyboardController destruction, controller can be set to null. |
| if (!controller) { |
| DCHECK(keyboard_controller()); |
| keyboard_controller()->RemoveObserver(observer_.get()); |
| KeyboardUI::SetController(nullptr); |
| return; |
| } |
| KeyboardUI::SetController(controller); |
| observer_ = |
| std::make_unique<AshKeyboardControllerObserver>(browser_context()); |
| keyboard_controller()->AddObserver(observer_.get()); |
| } |
| |
| void ChromeKeyboardUI::ShowKeyboardContainer(aura::Window* container) { |
| KeyboardUI::ShowKeyboardContainer(container); |
| } |
| |
| void ChromeKeyboardUI::RenderViewCreated( |
| content::RenderViewHost* render_view_host) { |
| content::HostZoomMap* zoom_map = |
| content::HostZoomMap::GetDefaultForBrowserContext(browser_context()); |
| DCHECK(zoom_map); |
| int render_process_id = render_view_host->GetProcess()->GetID(); |
| int render_view_id = render_view_host->GetRoutingID(); |
| zoom_map->SetTemporaryZoomLevel(render_process_id, render_view_id, 0); |
| } |
| |
| void ChromeKeyboardUI::DidFinishLoad( |
| content::RenderFrameHost* render_frame_host, |
| const GURL& validated_url) { |
| keyboard_controller()->NotifyContentsLoaded(); |
| } |