chromium/chrome/browser/ui/webui/ash/settings/pages/device/device_keyboard_handler_unittest.cc

// Copyright 2017 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/webui/ash/settings/pages/device/device_keyboard_handler.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/observer_list.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "content/public/test/test_web_ui.h"
#include "device/udev_linux/fake_udev_loader.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/devices/device_data_manager_test_api.h"
#include "ui/events/devices/input_device.h"

namespace ash::settings {

namespace {

class TestKeyboardHandler : public KeyboardHandler {
 public:
  // Pull WebUIMessageHandler::set_web_ui() into public so tests can call it.
  using KeyboardHandler::set_web_ui;
};

}  // namespace

class KeyboardHandlerTest : public ChromeAshTestBase {
 public:
  KeyboardHandlerTest() : handler_test_api_(&handler_) {
    handler_.set_web_ui(&web_ui_);
    handler_.RegisterMessages();
    handler_.AllowJavascriptForTesting();

    // Make sure that we start out without any keyboards reported.
    device_data_manager_test_api_.SetKeyboardDevices({});
  }

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

 protected:
  // Updates out-params from the last message sent to WebUI about a change to
  // which keys should be shown. False is returned if the message was invalid or
  // not found.
  [[nodiscard]] bool GetLastShowKeysChangedMessage(
      bool* has_caps_lock_out,
      bool* has_external_meta_key_out,
      bool* has_apple_command_key_out,
      bool* has_internal_search_out,
      bool* has_assistant_key_out) {
    for (const std::unique_ptr<content::TestWebUI::CallData>& data :
         base::Reversed(web_ui_.call_data())) {
      const std::string* name = data->arg1()->GetIfString();
      if (data->function_name() != "cr.webUIListenerCallback" || !name ||
          *name != KeyboardHandler::kShowKeysChangedName) {
        continue;
      }

      if (!data->arg2() || !data->arg2()->is_dict()) {
        return false;
      }

      const base::Value::Dict& keyboard_params = data->arg2()->GetDict();
      const std::vector<std::pair<std::string, bool*>> path_to_out_param = {
          {"showCapsLock", has_caps_lock_out},
          {"showExternalMetaKey", has_external_meta_key_out},
          {"showAppleCommandKey", has_apple_command_key_out},
          {"hasLauncherKey", has_internal_search_out},
          {"hasAssistantKey", has_assistant_key_out},
      };

      for (const auto& pair : path_to_out_param) {
        auto* found = keyboard_params.Find(pair.first);
        if (!found) {
          return false;
        }

        *(pair.second) = found->GetBool();
      }

      return true;
    }
    return false;
  }

  // Returns true if the last keys-changed message reported that a Caps Lock key
  // is present and false otherwise. A failure is added if a message wasn't
  // found.
  bool HasCapsLock() {
    bool has_caps_lock = false;
    bool ignored = false;
    if (!GetLastShowKeysChangedMessage(&has_caps_lock, &ignored, &ignored,
                                       &ignored, &ignored)) {
      ADD_FAILURE() << "Didn't get " << KeyboardHandler::kShowKeysChangedName;
      return false;
    }
    return has_caps_lock;
  }

  // Returns true if the last keys-changed message reported that a Meta key on
  // an external keyboard is present and false otherwise. A failure is added if
  // a message wasn't found.
  bool HasExternalMetaKey() {
    bool has_external_meta = false;
    bool ignored = false;
    if (!GetLastShowKeysChangedMessage(&ignored, &has_external_meta, &ignored,
                                       &ignored, &ignored)) {
      ADD_FAILURE() << "Didn't get " << KeyboardHandler::kShowKeysChangedName;
      return false;
    }
    return has_external_meta;
  }

  // Returns true if the last keys-changed message reported that a Command key
  // on an Apple keyboard is present and false otherwise. A failure is added if
  // a message wasn't found.
  bool HasAppleCommandKey() {
    bool has_apple_command_key = false;
    bool ignored = false;
    if (!GetLastShowKeysChangedMessage(
            &ignored, &ignored, &has_apple_command_key, &ignored, &ignored)) {
      ADD_FAILURE() << "Didn't get " << KeyboardHandler::kShowKeysChangedName;
      return false;
    }
    return has_apple_command_key;
  }

  // Returns true if the last keys-changed message reported that the device has
  // an Launcher key remap option. This applies only to ChromeOS keyboards.
  // A failure is added if a message wasn't found.
  bool HasLauncherKey() {
    bool has_launcher_key = false;
    bool ignored = false;
    if (!GetLastShowKeysChangedMessage(&ignored, &ignored, &ignored,
                                       &has_launcher_key, &ignored)) {
      ADD_FAILURE() << "Didn't get " << KeyboardHandler::kShowKeysChangedName;
      return false;
    }
    return has_launcher_key;
  }

  // Returns true if the last keys-changed message reported that the device has
  // an assistant key on its keyboard and otherwise false. A failure is added
  // if a message wasn't found.
  bool HasAssistantKey() {
    bool has_assistant_key = false;
    bool ignored = false;
    if (!GetLastShowKeysChangedMessage(&ignored, &ignored, &ignored, &ignored,
                                       &has_assistant_key)) {
      ADD_FAILURE() << "Didn't get " << KeyboardHandler::kShowKeysChangedName;
      return false;
    }
    return has_assistant_key;
  }

  ui::DeviceDataManagerTestApi device_data_manager_test_api_;
  content::TestWebUI web_ui_;
  TestKeyboardHandler handler_;
  KeyboardHandler::TestAPI handler_test_api_;
};

TEST_F(KeyboardHandlerTest, DefaultKeys) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      switches::kHasChromeOSKeyboard);
  handler_test_api_.Initialize();
  EXPECT_FALSE(HasLauncherKey());
  EXPECT_FALSE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());
}

TEST_F(KeyboardHandlerTest, NonChromeOSKeyboard) {
  // If kHasChromeOSKeyboard isn't passed, we should assume there's a Caps Lock
  // key.
  handler_test_api_.Initialize();
  EXPECT_FALSE(HasLauncherKey());
  EXPECT_TRUE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());
}

TEST_F(KeyboardHandlerTest, ExternalKeyboard) {
  auto fake_udev = std::make_unique<testing::FakeUdevLoader>();

  // Standard internal keyboard on x86 device.
  const ui::KeyboardDevice internal_kbd(
      1, ui::INPUT_DEVICE_INTERNAL, "AT Translated Set 2 keyboard", "",
      base::FilePath("/devices/platform/i8042/serio0/input/input1"), 1, 1,
      0xab41);
  fake_udev->AddFakeDevice(internal_kbd.name, internal_kbd.sys_path.value(),
                           /*subsystem=*/"input", /*devnode=*/std::nullopt,
                           /*devtype=*/std::nullopt, /*sysattrs=*/{},
                           /*properties=*/{});
  // Generic external USB keyboard.
  const ui::KeyboardDevice external_generic_kbd(
      2, ui::INPUT_DEVICE_USB, "Logitech USB Keyboard", "",
      base::FilePath("/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/"
                     "0003:046D:C31C.0007/"
                     "input/input2"),
      0x046d, 0xc31c, 0x0111);
  fake_udev->AddFakeDevice(external_generic_kbd.name,
                           external_generic_kbd.sys_path.value(),
                           /*subsystem=*/"input", /*devnode=*/std::nullopt,
                           /*devtype=*/std::nullopt, /*sysattrs=*/{},
                           /*properties=*/{});
  // Apple keyboard.
  const ui::KeyboardDevice external_apple_kbd(
      3, ui::INPUT_DEVICE_USB, "Apple Inc. Apple Keyboard", "",
      base::FilePath("/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/"
                     "0003:05AC:026C.000A/input/input3"),
      0x05ac, 0x026c, 0x0111);
  fake_udev->AddFakeDevice(external_apple_kbd.name,
                           external_apple_kbd.sys_path.value(),
                           /*subsystem=*/"input", /*devnode=*/std::nullopt,
                           /*devtype=*/std::nullopt, /*sysattrs=*/{},
                           /*properties=*/{});
  // Chrome OS external USB keyboard.
  const ui::KeyboardDevice external_chromeos_kbd(
      4, ui::INPUT_DEVICE_USB, "LG USB Keyboard", "",
      base::FilePath("/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/"
                     "0003:04CA:0082.000B/input/input4"),
      0x04ca, 0x0082, 0x0111);
  fake_udev->AddFakeDevice(
      external_chromeos_kbd.name, external_chromeos_kbd.sys_path.value(),
      /*subsystem=*/"input", /*devnode=*/std::nullopt,
      /*devtype=*/std::nullopt, /*sysattrs=*/{},
      /*properties=*/{{"CROS_KEYBOARD_TOP_ROW_LAYOUT", "1"}});

  // Chrome OS external Bluetooth keyboard.
  const ui::KeyboardDevice external_bt_chromeos_kbd(
      4, ui::INPUT_DEVICE_BLUETOOTH, "LG BT Keyboard", "",
      base::FilePath("/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/"
                     "0003:04CA:0082.000B/input/input5"),
      0x04ca, 0x0082, 0x0111);
  fake_udev->AddFakeDevice(
      external_bt_chromeos_kbd.name, external_bt_chromeos_kbd.sys_path.value(),
      /*subsystem=*/"input", /*devnode=*/std::nullopt,
      /*devtype=*/std::nullopt, /*sysattrs=*/{},
      /*properties=*/{{"CROS_KEYBOARD_TOP_ROW_LAYOUT", "1"}});

  // An internal keyboard shouldn't change the defaults.
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      switches::kHasChromeOSKeyboard);
  device_data_manager_test_api_.SetKeyboardDevices({internal_kbd});
  handler_test_api_.Initialize();
  EXPECT_TRUE(HasLauncherKey());
  EXPECT_FALSE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Simulate an external keyboard being connected. We should assume there's a
  // Caps Lock and Meta keys now.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{internal_kbd, external_generic_kbd});
  EXPECT_TRUE(HasLauncherKey());
  EXPECT_TRUE(HasCapsLock());
  EXPECT_TRUE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // However when connecting external ChromeOS-branded keyboard, we should not
  // see neither CapsLock not meta keys.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{internal_kbd, external_chromeos_kbd});
  EXPECT_TRUE(HasLauncherKey());
  EXPECT_FALSE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Connecting external Bluetooth ChromeOS-branded keyboard, we should not
  // see neither CapsLock not meta keys.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{external_bt_chromeos_kbd});
  EXPECT_TRUE(HasLauncherKey());
  EXPECT_FALSE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Simulate an external Apple keyboard being connected. Now users can remap
  // the command key.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{internal_kbd, external_apple_kbd});
  EXPECT_TRUE(HasLauncherKey());
  EXPECT_TRUE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_TRUE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Simulate two external keyboards (Apple and non-Apple) are connected at the
  // same time.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{external_generic_kbd,
                                      external_apple_kbd});
  EXPECT_FALSE(HasLauncherKey());
  EXPECT_TRUE(HasCapsLock());
  EXPECT_TRUE(HasExternalMetaKey());
  EXPECT_TRUE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Some keyboard devices don't report the string "keyboard" as part of their
  // device names. Those should also be detected as external keyboards, and
  // should show the capslock and external meta remapping.
  // https://crbug.com/834594.
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {6, ui::INPUT_DEVICE_USB, "Topre Corporation Realforce 87", "",
           external_generic_kbd.sys_path, 0x046d, 0xc31c, 0x0111}});
  EXPECT_FALSE(HasLauncherKey());
  EXPECT_TRUE(HasCapsLock());
  EXPECT_TRUE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());

  // Disconnect the external keyboard and check that the key goes away.
  device_data_manager_test_api_.SetKeyboardDevices({});
  EXPECT_FALSE(HasLauncherKey());
  EXPECT_FALSE(HasCapsLock());
  EXPECT_FALSE(HasExternalMetaKey());
  EXPECT_FALSE(HasAppleCommandKey());
  EXPECT_FALSE(HasAssistantKey());
}

}  // namespace ash::settings