blob: 4fe10b88a5b5f8acef2c2efa7520d8369ab925e5 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package input
import (
"bufio"
"bytes"
"context"
"regexp"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
)
// TopRowLayout represents the top row layout of the Chromebook keyboard.
// Each entry represents the key it is mapped to. An empty string means that is is not mapped.
type TopRowLayout struct {
BrowserBack string
BrowserForward string
BrowserRefresh string
ZoomToggle string
SelectTask string
Screenshot string
BrightnessDown string
BrightnessUp string
MediaPlayPause string
VolumeMute string
VolumeDown string
VolumeUp string
MediaLaunchApp string
}
// KeyboardTopRowLayout returns the layout of the top row (function keys) for a given keyboard.
// This is because not all Chromebook keyboards have the same functionality associated to the functions keys.
// As an example, the Toggle Zoom key could be mapped to F3 or F4 depending on the Chromebook model.
func KeyboardTopRowLayout(ctx context.Context, ew *KeyboardEventWriter) (*TopRowLayout, error) {
// "mapping 1" and "mapping 2" taken from:
// https://cs.chromium.org/chromium/src/ui/chromeos/events/event_rewriter_chromeos.cc?l=1143&rcl=3028a8be77afd57282d664b6bb07f6d4d01edc55
mapping1 := TopRowLayout{
BrowserBack: "F1",
BrowserForward: "F2",
BrowserRefresh: "F3",
ZoomToggle: "F4",
SelectTask: "F5",
BrightnessDown: "F6",
BrightnessUp: "F7",
VolumeMute: "F8",
VolumeDown: "F9",
VolumeUp: "F10",
}
// BROWSER_FORWARD removed, MEDIA_PLAY_PAUSE added.
mapping2 := TopRowLayout{
BrowserBack: "F1",
BrowserRefresh: "F2",
ZoomToggle: "F3",
SelectTask: "F4",
BrightnessDown: "F5",
BrightnessUp: "F6",
MediaPlayPause: "F7",
VolumeMute: "F8",
VolumeDown: "F9",
VolumeUp: "F10",
}
// Wilco mappings taken from:
// https://source.chromium.org/chromium/chromium/src/+/HEAD:ui/chromeos/events/event_rewriter_chromeos.cc;drc=3e2b7d89ce6261e00e6e723c13c52d0d41bcc69e;l=1599
// MEDIA_PLAY_PAUSE removed, MEDIA_LAUNCH_APP2 added.
mappingWilco := TopRowLayout{
BrowserBack: "search+F1",
BrowserRefresh: "search+F2",
ZoomToggle: "search+F3",
SelectTask: "search+F4",
BrightnessDown: "search+F5",
BrightnessUp: "search+F6",
VolumeMute: "search+F7",
VolumeDown: "search+F8",
VolumeUp: "search+F9",
MediaLaunchApp: "search+F12",
}
// This is the minimal set of required top row keys for custom top row
// layouts.
mappingCustom := TopRowLayout{
BrowserBack: "back",
BrowserRefresh: "refresh",
ZoomToggle: "fullscreen",
SelectTask: "scale",
Screenshot: "sysrq",
BrightnessDown: "brightnessdown",
BrightnessUp: "brightnessup",
VolumeMute: "mute",
VolumeDown: "volumedown",
VolumeUp: "volumeup",
}
props, err := udevProperties(ctx, ew.Device())
if err != nil {
return nil, err
}
attrs, err := udevAttributes(ctx, ew.Device())
if err != nil {
return nil, err
}
// Logic taken from here:
// https://source.chromium.org/chromium/chromium/src/+/HEAD:ui/chromeos/events/event_rewriter_chromeos.h;l=56;drc=3e2b7d89ce6261e00e6e723c13c52d0d41bcc69e
if _, ok := attrs["function_row_physmap"]; ok {
return &mappingCustom, nil
}
if val, ok := props["CROS_KEYBOARD_TOP_ROW_LAYOUT"]; ok {
switch val {
case "1":
return &mapping1, nil
case "2":
return &mapping2, nil
case "3", "4":
return &mappingWilco, nil
default:
return nil, errors.Errorf("unexpected CROS_KEYBOARD_ROW_LAYOUT: got %s, want [1-4]", val)
}
}
// If keyboard cannot be identified, return mappings1 as defined here:
// https://source.chromium.org/chromium/chromium/src/+/HEAD:ui/chromeos/events/event_rewriter_chromeos.h;l=172;drc=c537d05a0cc7b74258fe1474260094923b1e4f68
return &mapping1, nil
}
// udevAttributes returns the attributes associated to a certain Linux udev device.
func udevAttributes(ctx context.Context, devicePath string) (map[string]string, error) {
cmd := testexec.CommandContext(ctx, "udevadm", "info", "--attribute-walk", "property", "--name", devicePath)
out, err := cmd.Output()
if err != nil {
cmd.DumpLog(ctx)
return nil, errors.Errorf("failed running %q", strings.Join(cmd.Args, " "))
}
return parseUdev(out, `^ATTRS{([a-z0-9_]+)}=="(.*)"$`)
}
// udevProperties returns the properties associated to a certain Linux udev device.
func udevProperties(ctx context.Context, devicePath string) (map[string]string, error) {
cmd := testexec.CommandContext(ctx, "udevadm", "info", "--query", "property", "--name", devicePath)
out, err := cmd.Output()
if err != nil {
cmd.DumpLog(ctx)
return nil, errors.Errorf("failed running %q", strings.Join(cmd.Args, " "))
}
return parseUdev(out, `^([A-Z0-9_]+)\s*=\s*(.*)$`)
}
// parseUdev parses the raw output from udevadm and converts it into a key-value map.
func parseUdev(r []byte, matchPattern string) (map[string]string, error) {
// lineRe matches a key-value line.
var lineRe = regexp.MustCompile(matchPattern)
kvs := make(map[string]string)
sc := bufio.NewScanner(bytes.NewReader(r))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
m := lineRe.FindStringSubmatch(line)
if m == nil {
continue
}
kvs[m[1]] = m[2]
}
if err := sc.Err(); err != nil {
return nil, err
}
return kvs, nil
}