blob: bbce7bda6ebe438a511519a9e5f11ae8b99c95de [file] [log] [blame]
// Copyright 2016 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 "ui/base/win/osk_display_manager.h"
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <shobjidl.h> // Must be before propkey.
#include "base/bind.h"
#include "base/debug/leak_annotations.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/win/registry.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "ui/base/win/hidden_window.h"
#include "ui/base/win/osk_display_observer.h"
#include "ui/display/win/dpi.h"
#include "ui/gfx/geometry/dip_util.h"
namespace {
constexpr int kCheckOSKDelayMs = 1000;
constexpr int kDismissKeyboardRetryTimeoutMs = 100;
constexpr int kDismissKeyboardMaxRetries = 5;
constexpr wchar_t kOSKClassName[] = L"IPTip_Main_Window";
constexpr wchar_t kWindows8OSKRegPath[] =
L"Software\\Classes\\CLSID\\{054AAE20-4BEA-4347-8A35-64A533254A9D}"
L"\\LocalServer32";
} // namespace
namespace ui {
// This class provides functionality to detect when the on screen keyboard
// is displayed and move the main window up if it is obscured by the keyboard.
class OnScreenKeyboardDetector {
public:
OnScreenKeyboardDetector();
~OnScreenKeyboardDetector();
// Schedules a delayed task which detects if the on screen keyboard was
// displayed.
void DetectKeyboard(HWND main_window);
// Dismisses the on screen keyboard. If a call to display the keyboard was
// made, this function waits for the keyboard to become visible by retrying
// upto a maximum of kDismissKeyboardMaxRetries.
bool DismissKeyboard();
// Add/Remove keyboard observers.
// Please note that this class does not track the |observer| destruction. It
// is upto the classes which set up these observers to remove them when they
// are destroyed.
void AddObserver(OnScreenKeyboardObserver* observer);
void RemoveObserver(OnScreenKeyboardObserver* observer);
// Returns true if the osk is visible. Sets osk bounding rect if non-null
static bool IsKeyboardVisible(gfx::Rect* osk_bounding_rect);
private:
// Executes as a task and detects if the on screen keyboard is displayed.
// Once the keyboard is displayed it schedules the HideIfNecessary() task to
// detect when the keyboard is or should be hidden.
void CheckIfKeyboardVisible();
// Executes as a task and detects if the keyboard was hidden or should be
// hidden.
void HideIfNecessary();
// Notifies observers that the keyboard was displayed.
// A recurring task HideIfNecessary() is started to detect when the OSK
// disappears.
void HandleKeyboardVisible();
// Notifies observers that the keyboard was hidden.
// The observer list is cleared out after this notification.
void HandleKeyboardHidden();
// Removes all observers from the list.
void ClearObservers();
// The main window which displays the on screen keyboard.
HWND main_window_ = nullptr;
// Tracks if the keyboard was displayed.
bool osk_visible_notification_received_ = false;
// The keyboard dimensions in pixels.
gfx::Rect osk_rect_pixels_;
// Set to true if a call to DetectKeyboard() was made.
bool keyboard_detect_requested_ = false;
// Contains the number of attempts made to dismiss the keyboard. Please refer
// to the DismissKeyboard() function for more information.
int keyboard_dismiss_retry_count_ = 0;
base::ObserverList<OnScreenKeyboardObserver, false> observers_;
// Should be the last member in the class. Helps ensure that tasks spawned
// by this class instance are canceled when it is destroyed.
base::WeakPtrFactory<OnScreenKeyboardDetector> keyboard_detector_factory_;
DISALLOW_COPY_AND_ASSIGN(OnScreenKeyboardDetector);
};
// OnScreenKeyboardDetector member definitions.
OnScreenKeyboardDetector::OnScreenKeyboardDetector()
: keyboard_detector_factory_(this) {}
OnScreenKeyboardDetector::~OnScreenKeyboardDetector() {
ClearObservers();
}
void OnScreenKeyboardDetector::DetectKeyboard(HWND main_window) {
main_window_ = main_window;
keyboard_detect_requested_ = true;
// The keyboard is displayed by TabTip.exe which is launched via a
// ShellExecute call in the
// OnScreenKeyboardDisplayManager::DisplayVirtualKeyboard() function. We use
// a delayed task to check if the keyboard is visible because of the possible
// delay between the ShellExecute call and the keyboard becoming visible.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(&OnScreenKeyboardDetector::CheckIfKeyboardVisible,
keyboard_detector_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kCheckOSKDelayMs));
}
bool OnScreenKeyboardDetector::DismissKeyboard() {
// We dismiss the virtual keyboard by generating the ESC keystroke
// programmatically.
HWND osk = ::FindWindow(kOSKClassName, nullptr);
if (::IsWindow(osk) && ::IsWindowEnabled(osk)) {
keyboard_detect_requested_ = false;
keyboard_dismiss_retry_count_ = 0;
HandleKeyboardHidden();
PostMessage(osk, WM_SYSCOMMAND, SC_CLOSE, 0);
return true;
} else if (keyboard_detect_requested_) {
if (keyboard_dismiss_retry_count_ < kDismissKeyboardMaxRetries) {
keyboard_dismiss_retry_count_++;
// Please refer to the comments in the DetectKeyboard() function for more
// information as to why we need a delayed task here.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(base::IgnoreResult(
&OnScreenKeyboardDetector::DismissKeyboard),
keyboard_detector_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kDismissKeyboardRetryTimeoutMs));
} else {
keyboard_dismiss_retry_count_ = 0;
}
}
return false;
}
void OnScreenKeyboardDetector::AddObserver(OnScreenKeyboardObserver* observer) {
observers_.AddObserver(observer);
}
void OnScreenKeyboardDetector::RemoveObserver(
OnScreenKeyboardObserver* observer) {
observers_.RemoveObserver(observer);
}
// static
bool OnScreenKeyboardDetector::IsKeyboardVisible(gfx::Rect* osk_bounding_rect) {
HWND osk = ::FindWindow(kOSKClassName, nullptr);
if (!::IsWindow(osk))
return false;
if (osk_bounding_rect) {
RECT osk_rect = {};
::GetWindowRect(osk, &osk_rect);
*osk_bounding_rect = gfx::Rect(osk_rect);
}
return ::IsWindowVisible(osk) && ::IsWindowEnabled(osk);
}
void OnScreenKeyboardDetector::CheckIfKeyboardVisible() {
if (IsKeyboardVisible(&osk_rect_pixels_)) {
if (!osk_visible_notification_received_)
HandleKeyboardVisible();
} else {
DVLOG(1) << "OSK did not come up in 1 second. Something wrong.";
}
}
void OnScreenKeyboardDetector::HideIfNecessary() {
HWND osk = ::FindWindow(kOSKClassName, nullptr);
if (!::IsWindow(osk))
return;
// Three cases here.
// 1. OSK was hidden because the user dismissed it.
// 2. We are no longer in the foreground.
// 3. The OSK is still visible.
// In the first case we just have to notify the observers that the OSK was
// hidden.
// In the second case we need to dismiss the OSK which internally will
// notify the observers about the OSK being hidden.
if (!::IsWindowEnabled(osk)) {
if (osk_visible_notification_received_) {
if (main_window_ == ::GetForegroundWindow()) {
DVLOG(1) << "OSK window hidden while we are in the foreground.";
HandleKeyboardHidden();
}
}
} else if (main_window_ != ::GetForegroundWindow()) {
if (osk_visible_notification_received_) {
DVLOG(1) << "We are no longer in the foreground. Dismising OSK.";
DismissKeyboard();
}
} else {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(&OnScreenKeyboardDetector::HideIfNecessary,
keyboard_detector_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kCheckOSKDelayMs));
}
}
void OnScreenKeyboardDetector::HandleKeyboardVisible() {
DCHECK(!osk_visible_notification_received_);
osk_visible_notification_received_ = true;
for (OnScreenKeyboardObserver& observer : observers_)
observer.OnKeyboardVisible(osk_rect_pixels_);
// Now that the keyboard is visible, run the task to detect if it was hidden.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, base::Bind(&OnScreenKeyboardDetector::HideIfNecessary,
keyboard_detector_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kCheckOSKDelayMs));
}
void OnScreenKeyboardDetector::HandleKeyboardHidden() {
osk_visible_notification_received_ = false;
for (OnScreenKeyboardObserver& observer : observers_)
observer.OnKeyboardHidden(osk_rect_pixels_);
ClearObservers();
}
void OnScreenKeyboardDetector::ClearObservers() {
for (auto& observer : observers_)
RemoveObserver(&observer);
}
// OnScreenKeyboardDisplayManager member definitions.
OnScreenKeyboardDisplayManager::OnScreenKeyboardDisplayManager() {}
OnScreenKeyboardDisplayManager::~OnScreenKeyboardDisplayManager() {}
OnScreenKeyboardDisplayManager* OnScreenKeyboardDisplayManager::GetInstance() {
static OnScreenKeyboardDisplayManager* instance = nullptr;
if (!instance) {
instance = new OnScreenKeyboardDisplayManager;
ANNOTATE_LEAKING_OBJECT_PTR(instance);
}
return instance;
}
bool OnScreenKeyboardDisplayManager::DisplayVirtualKeyboard(
OnScreenKeyboardObserver* observer) {
if (base::win::GetVersion() < base::win::VERSION_WIN8)
return false;
if (base::win::IsKeyboardPresentOnSlate(nullptr, ui::GetHiddenWindow()))
return false;
if (osk_path_.empty() && !GetOSKPath(&osk_path_)) {
DLOG(WARNING) << "Failed to get on screen keyboard path from registry";
return false;
}
HINSTANCE ret = ::ShellExecuteW(nullptr, L"", osk_path_.c_str(), nullptr,
nullptr, SW_SHOW);
bool success = reinterpret_cast<intptr_t>(ret) > 32;
if (success) {
// If multiple calls to DisplayVirtualKeyboard occur one after the other,
// the last observer would be the one to get notifications.
keyboard_detector_.reset(new OnScreenKeyboardDetector);
if (observer)
keyboard_detector_->AddObserver(observer);
keyboard_detector_->DetectKeyboard(::GetForegroundWindow());
}
return success;
}
bool OnScreenKeyboardDisplayManager::DismissVirtualKeyboard() {
if (base::win::GetVersion() < base::win::VERSION_WIN8)
return false;
return keyboard_detector_ ? keyboard_detector_->DismissKeyboard() : false;
}
void OnScreenKeyboardDisplayManager::RemoveObserver(
OnScreenKeyboardObserver* observer) {
if (keyboard_detector_)
keyboard_detector_->RemoveObserver(observer);
}
bool OnScreenKeyboardDisplayManager::GetOSKPath(base::string16* osk_path) {
DCHECK(osk_path);
// We need to launch TabTip.exe from the location specified under the
// LocalServer32 key for the {{054AAE20-4BEA-4347-8A35-64A533254A9D}}
// CLSID.
// TabTip.exe is typically found at
// c:\program files\common files\microsoft shared\ink on English Windows.
// We don't want to launch TabTip.exe from
// c:\program files (x86)\common files\microsoft shared\ink. This path is
// normally found on 64 bit Windows.
base::win::RegKey key(HKEY_LOCAL_MACHINE, kWindows8OSKRegPath,
KEY_READ | KEY_WOW64_64KEY);
DWORD osk_path_length = 1024;
if (key.ReadValue(nullptr, base::WriteInto(osk_path, osk_path_length),
&osk_path_length, nullptr) != ERROR_SUCCESS) {
return false;
}
osk_path->resize(base::string16::traits_type::length(osk_path->c_str()));
*osk_path = base::ToLowerASCII(*osk_path);
size_t common_program_files_offset = osk_path->find(L"%commonprogramfiles%");
// Typically the path to TabTip.exe read from the registry will start with
// %CommonProgramFiles% which needs to be replaced with the corrsponding
// expanded string.
// If the path does not begin with %CommonProgramFiles% we use it as is.
if (common_program_files_offset != base::string16::npos) {
// Preserve the beginning quote in the path.
osk_path->erase(common_program_files_offset,
wcslen(L"%commonprogramfiles%"));
// The path read from the registry contains the %CommonProgramFiles%
// environment variable prefix. On 64 bit Windows the SHGetKnownFolderPath
// function returns the common program files path with the X86 suffix for
// the FOLDERID_ProgramFilesCommon value.
// To get the correct path to TabTip.exe we first read the environment
// variable CommonProgramW6432 which points to the desired common
// files path. Failing that we fallback to the SHGetKnownFolderPath API.
// We then replace the %CommonProgramFiles% value with the actual common
// files path found in the process.
base::string16 common_program_files_path;
DWORD buffer_size =
GetEnvironmentVariable(L"CommonProgramW6432", nullptr, 0);
if (buffer_size) {
GetEnvironmentVariable(
L"CommonProgramW6432",
base::WriteInto(&common_program_files_path, buffer_size),
buffer_size);
DCHECK(!common_program_files_path.empty());
} else {
base::win::ScopedCoMem<wchar_t> common_program_files;
if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, nullptr,
&common_program_files))) {
return false;
}
common_program_files_path = common_program_files;
}
osk_path->insert(common_program_files_offset, common_program_files_path);
}
return !osk_path->empty();
}
bool OnScreenKeyboardDisplayManager::IsKeyboardVisible() const {
return OnScreenKeyboardDetector::IsKeyboardVisible(nullptr);
}
} // namespace ui