// 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/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);

 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() {}

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);
}

void OnScreenKeyboardDetector::CheckIfKeyboardVisible() {
  HWND osk = ::FindWindow(kOSKClassName, nullptr);
  if (!::IsWindow(osk))
    return;

  RECT osk_rect = {};
  ::GetWindowRect(osk, &osk_rect);
  osk_rect_pixels_ = gfx::Rect(osk_rect);
  if (::IsWindowVisible(osk) && ::IsWindowEnabled(osk)) {
    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))
    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();
}

}  // namespace ui
