chromium/ui/base/ime/win/on_screen_keyboard_display_manager_input_pane.cc

// 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 "ui/base/ime/win/on_screen_keyboard_display_manager_input_pane.h"

#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "base/win/com_init_util.h"
#include "base/win/core_winrt_util.h"
#include "base/win/windows_version.h"
#include "ui/base/ime/virtual_keyboard_controller_observer.h"

namespace ui {

// VirtualKeyboardInputPane class is used to store all the COM objects and
// control their lifetime, so all the COM processing is on a background
// thread.
class OnScreenKeyboardDisplayManagerInputPane::VirtualKeyboardInputPane
    : public base::RefCountedThreadSafe<VirtualKeyboardInputPane> {
 public:
  explicit VirtualKeyboardInputPane(
      const scoped_refptr<base::SingleThreadTaskRunner> task_runner)
      : main_task_runner_(task_runner) {}

  VirtualKeyboardInputPane(const VirtualKeyboardInputPane&) = delete;
  VirtualKeyboardInputPane& operator=(const VirtualKeyboardInputPane&) = delete;

  void InitVirtualKeyboardInputPaneInstance(
      base::WeakPtr<OnScreenKeyboardDisplayManagerInputPane>
          input_pane_weak_ptr) {
    keyboard_input_pane_weak_ptr_ = input_pane_weak_ptr;
  }

  // Set the virtual keyboard input pane for |OnScreenKeyboardTest| tests.
  void SetInputPaneForTestingInBackgroundThread(
      Microsoft::WRL::ComPtr<ABI::Windows::UI::ViewManagement::IInputPane>
          pane) {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    DCHECK(!input_pane_);
    input_pane_ = pane;
    HRESULT hr = input_pane_.As(&input_pane2_);
    DCHECK(SUCCEEDED(hr));

    AddCallbacksOnInputPaneShownOrHiddenInBackgroundThread();
  }

  void TryShowInBackgroundThread(HWND hwnd) {
    // TODO(crbug.com/40110609): Remove this once TSF fix for input pane policy
    // is serviced
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    if (!EnsureInputPanePointersInBackgroundThread(hwnd))
      return;
    boolean res;
    TRACE_EVENT0("vk",
                 "OnScreenKeyboardDisplayManagerInputPane::"
                 "VirtualKeyboardInputPane::TryShowInBackgroundThread");
    input_pane2_->TryShow(&res);
  }

  void TryHideInBackgroundThread(HWND hwnd) {
    // TODO(crbug.com/40110609): Remove this once TSF fix for input pane policy
    // is serviced
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    if (!EnsureInputPanePointersInBackgroundThread(hwnd))
      return;
    boolean res;
    TRACE_EVENT0("vk",
                 "OnScreenKeyboardDisplayManagerInputPane::"
                 "VirtualKeyboardInputPane::TryHideInBackgroundThread");
    input_pane2_->TryHide(&res);
  }

 private:
  friend class base::RefCountedThreadSafe<VirtualKeyboardInputPane>;

  ~VirtualKeyboardInputPane() {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    if (input_pane_) {
      // Remove the callbacks that were registered.
      input_pane_->remove_Showing(show_event_token_);
      input_pane_->remove_Hiding(hide_event_token_);
    }
  }

  bool EnsureInputPanePointersInBackgroundThread(HWND hwnd) {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    if (input_pane2_)
      return true;

    base::win::AssertComApartmentType(base::win::ComApartmentType::STA);

    base::win::ScopedHString input_pane_guid = base::win::ScopedHString::Create(
        RuntimeClass_Windows_UI_ViewManagement_InputPane);
    Microsoft::WRL::ComPtr<IInputPaneInterop> input_pane_interop;
    HRESULT hr = base::win::RoGetActivationFactory(
        input_pane_guid.get(), IID_PPV_ARGS(&input_pane_interop));
    if (FAILED(hr))
      return false;

    hr = input_pane_interop->GetForWindow(hwnd, IID_PPV_ARGS(&input_pane_));
    if (FAILED(hr))
      return false;

    if (FAILED(input_pane_.As(&input_pane2_))) {
      input_pane_.Reset();
      return false;
    }

    AddCallbacksOnInputPaneShownOrHiddenInBackgroundThread();
    return true;
  }

  // Add callbacks to notify virtual keyboard observers when the virtual
  // keyboard is visible or hidden.
  void AddCallbacksOnInputPaneShownOrHiddenInBackgroundThread() {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    input_pane_->add_Showing(
        Microsoft::WRL::Callback<
            OnScreenKeyboardDisplayManagerInputPane::VirtualKeyboardInputPane::
                InputPaneEventHandler>(
            this, &OnScreenKeyboardDisplayManagerInputPane::
                      VirtualKeyboardInputPane::OnInputPaneShown)
            .Get(),
        &show_event_token_);

    input_pane_->add_Hiding(
        Microsoft::WRL::Callback<
            OnScreenKeyboardDisplayManagerInputPane::VirtualKeyboardInputPane::
                InputPaneEventHandler>(
            this, &OnScreenKeyboardDisplayManagerInputPane::
                      VirtualKeyboardInputPane::OnInputPaneHidden)
            .Get(),
        &hide_event_token_);
  }

  HRESULT OnInputPaneShown(
      ABI::Windows::UI::ViewManagement::IInputPane* pane,
      ABI::Windows::UI::ViewManagement::IInputPaneVisibilityEventArgs* args) {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    // Due to timing this could be called by the OS even when input_pane_
    // is null, so just bail out to avoid crashes.
    if (!input_pane_)
      return S_OK;

    ABI::Windows::Foundation::Rect rect;
    input_pane_->get_OccludedRect(&rect);
    gfx::Rect dip_rect(rect.X, rect.Y, rect.Width, rect.Height);
    TRACE_EVENT1("vk",
                 "OnScreenKeyboardDisplayManagerInputPane::"
                 "VirtualKeyboardInputPane::OnInputPaneShown",
                 "dip_rect", dip_rect.ToString());

    main_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&OnScreenKeyboardDisplayManagerInputPane::
                                      NotifyObserversOnKeyboardShown,
                                  keyboard_input_pane_weak_ptr_, dip_rect));
    return S_OK;
  }

  HRESULT OnInputPaneHidden(
      ABI::Windows::UI::ViewManagement::IInputPane* pane,
      ABI::Windows::UI::ViewManagement::IInputPaneVisibilityEventArgs* args) {
    DCHECK(!main_task_runner_->BelongsToCurrentThread());
    // Due to timing this could be called by the OS even when input_pane_
    // is null, so just bail out to avoid crashes.
    if (!input_pane_)
      return S_OK;

    TRACE_EVENT0("vk",
                 "OnScreenKeyboardDisplayManagerInputPane::"
                 "VirtualKeyboardInputPane::OnInputPaneHidden");
    main_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&OnScreenKeyboardDisplayManagerInputPane::
                                      NotifyObserversOnKeyboardHidden,
                                  keyboard_input_pane_weak_ptr_));
    return S_OK;
  }

  using InputPaneEventHandler = ABI::Windows::Foundation::ITypedEventHandler<
      ABI::Windows::UI::ViewManagement::InputPane*,
      ABI::Windows::UI::ViewManagement::InputPaneVisibilityEventArgs*>;

  // InputPane objects are owned by VirtualKeyboardInputPane class and their
  // functions are ran on a background thread.
  Microsoft::WRL::ComPtr<ABI::Windows::UI::ViewManagement::IInputPane>
      input_pane_;
  Microsoft::WRL::ComPtr<ABI::Windows::UI::ViewManagement::IInputPane2>
      input_pane2_;

  EventRegistrationToken show_event_token_;
  EventRegistrationToken hide_event_token_;

  // |main_task_runner_| and |keyboard_input_pane_weak_ptr_| are owned by
  // OnScreenKeyboardDisplayManagerInputPane class, and they are running on the
  // main thread, which are used to post task to the main thread from the
  // background thread.
  scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
  base::WeakPtr<OnScreenKeyboardDisplayManagerInputPane>
      keyboard_input_pane_weak_ptr_;
};

OnScreenKeyboardDisplayManagerInputPane::
    OnScreenKeyboardDisplayManagerInputPane(HWND hwnd)
    : hwnd_(hwnd),
      main_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
      background_task_runner_(base::ThreadPool::CreateCOMSTATaskRunner(
          {base::MayBlock(),
           // This TaskRunner runs tasks that wait for messages to be processed
           // on the main thread. During shutdown, the main thread stops
           // processing messages and as a result tasks on this TaskRunner may
           // hang. Use `CONTINUE_ON_SHUTDOWN` to let shutdown complete when
           // this happens (see crbug.com/40848571).
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})),
      virtual_keyboard_input_pane_(
          base::MakeRefCounted<OnScreenKeyboardDisplayManagerInputPane::
                                   VirtualKeyboardInputPane>(
              main_task_runner_)),
      is_keyboard_visible_(false) {
  DCHECK_GE(base::win::GetVersion(), base::win::Version::WIN10_RS1);
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  // 300ms is the timer we chose after experimenting with users on windows touch
  // devices.
  debouncer_ = std::make_unique<VirtualKeyboardDebounceTimer>(300);

  // We post the initiation of |virtual_keyboard_input_pane_| to the background
  // thread first, and any other tasks posted to the background thread are
  // executed after its initiation.
  background_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &OnScreenKeyboardDisplayManagerInputPane::VirtualKeyboardInputPane::
              InitVirtualKeyboardInputPaneInstance,
          base::RetainedRef(virtual_keyboard_input_pane_),
          weak_factory_.GetWeakPtr()));
}

void OnScreenKeyboardDisplayManagerInputPane::Run() {
  // Execute show() or hide() on the background thread after the debounce
  // expires.
  switch (last_vk_visibility_request_) {
    case mojom::VirtualKeyboardVisibilityRequest::SHOW: {
      background_task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(
              &OnScreenKeyboardDisplayManagerInputPane::
                  VirtualKeyboardInputPane::TryShowInBackgroundThread,
              base::RetainedRef(virtual_keyboard_input_pane_), hwnd_));
      break;
    }
    case mojom::VirtualKeyboardVisibilityRequest::HIDE: {
      background_task_runner_->PostTask(
          FROM_HERE,
          base::BindOnce(
              &OnScreenKeyboardDisplayManagerInputPane::
                  VirtualKeyboardInputPane::TryHideInBackgroundThread,
              base::RetainedRef(virtual_keyboard_input_pane_), hwnd_));
      break;
    }
    case mojom::VirtualKeyboardVisibilityRequest::NONE: {
      break;
    }
  }
  // Reset the VK visibility state to none so we can keep track of subsequent
  // API calls.
  last_vk_visibility_request_ = mojom::VirtualKeyboardVisibilityRequest::NONE;
}

bool OnScreenKeyboardDisplayManagerInputPane::DisplayVirtualKeyboard() {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  last_vk_visibility_request_ = mojom::VirtualKeyboardVisibilityRequest::SHOW;
  debouncer_->RequestRun(base::BindOnce(
      &OnScreenKeyboardDisplayManagerInputPane::Run, base::Unretained(this)));
  return true;
}

void OnScreenKeyboardDisplayManagerInputPane::DismissVirtualKeyboard() {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  last_vk_visibility_request_ = mojom::VirtualKeyboardVisibilityRequest::HIDE;
  debouncer_->RequestRun(base::BindOnce(
      &OnScreenKeyboardDisplayManagerInputPane::Run, base::Unretained(this)));
}

void OnScreenKeyboardDisplayManagerInputPane::AddObserver(
    VirtualKeyboardControllerObserver* observer) {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  observers_.AddObserver(observer);
}

void OnScreenKeyboardDisplayManagerInputPane::RemoveObserver(
    VirtualKeyboardControllerObserver* observer) {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  observers_.RemoveObserver(observer);
}

bool OnScreenKeyboardDisplayManagerInputPane::IsKeyboardVisible() {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  return is_keyboard_visible_;
}

void OnScreenKeyboardDisplayManagerInputPane::SetInputPaneForTesting(
    Microsoft::WRL::ComPtr<ABI::Windows::UI::ViewManagement::IInputPane> pane) {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  base::ThreadPool::CreateCOMSTATaskRunner(
      {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})
      ->PostTask(FROM_HERE,
                 base::BindOnce(
                     &OnScreenKeyboardDisplayManagerInputPane::
                         VirtualKeyboardInputPane::
                             SetInputPaneForTestingInBackgroundThread,
                     base::RetainedRef(virtual_keyboard_input_pane_), pane));
}

void OnScreenKeyboardDisplayManagerInputPane::NotifyObserversOnKeyboardShown(
    gfx::Rect dip_rect) {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  is_keyboard_visible_ = true;
  for (VirtualKeyboardControllerObserver& observer : observers_)
    observer.OnKeyboardVisible(dip_rect);
}

void OnScreenKeyboardDisplayManagerInputPane::
    NotifyObserversOnKeyboardHidden() {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  is_keyboard_visible_ = false;
  for (VirtualKeyboardControllerObserver& observer : observers_)
    observer.OnKeyboardHidden();
}

OnScreenKeyboardDisplayManagerInputPane::
    ~OnScreenKeyboardDisplayManagerInputPane() {
  DCHECK(main_task_runner_->BelongsToCurrentThread());
  // In-case there is a debouncer task running, cancel it.
  debouncer_->CancelRequest();
  if (virtual_keyboard_input_pane_.get()) {
    background_task_runner_->ReleaseSoon(
        FROM_HERE, std::move(virtual_keyboard_input_pane_));
  }
}

}  // namespace ui