blob: f9a504f151ecf87c1eab5b04de5f95e10410384a [file]
// Copyright 2017 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 display wraps the chrome.system.display API.
//
// Functions require a chrome.Conn with permission to use the chrome.system.display API.
// chrome.Chrome.TestAPIConn has such permission and may be passed here.
package display
import (
"context"
"math"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/coords"
"chromiumos/tast/testing"
)
// Insets holds onscreen insets.
// See https://developer.chrome.com/docs/extensions/reference/system_display/#type-Insets.
type Insets struct {
Left int `json:"left"`
Top int `json:"top"`
Right int `json:"right"`
Bottom int `json:"bottom"`
}
// DisplayMode holds a mode supported by the display.
// See https://developer.chrome.com/docs/extensions/reference/system_display/#type-DisplayMode.
type DisplayMode struct { // NOLINT
Width int `json:"width"`
Height int `json:"height"`
WidthInNativePixels int `json:"widthInNativePixels"`
HeightInNativePixels int `json:"heightInNativePixels"`
UIScale float64 `json:"uiScale,omitempty"`
DeviceScaleFactor float64 `json:"deviceScaleFactor"`
RefreshRate float64 `json:"refreshRate"`
IsNative bool `json:"isNative"`
IsSelected bool `json:"isSelected"`
IsInterlaced bool `json:"isInterlaced,omitempty"`
}
// Info holds information about a display and is returned by GetInfo.
// See https://developer.chrome.com/docs/extensions/reference/system_display/#type-DisplayUnitInfo.
type Info struct {
ID string `json:"id"`
Name string `json:"name"`
MirroringSourceID string `json:"mirroringSourceId"`
IsPrimary bool `json:"isPrimary"`
IsInternal bool `json:"isInternal"`
IsEnabled bool `json:"isEnabled"`
IsUnified bool `json:"isUnified"`
DPIX float64 `json:"dpiX"`
DPIY float64 `json:"dpiY"`
Rotation int `json:"rotation"`
Bounds coords.Rect `json:"bounds"`
Overscan *Insets `json:"overscan"`
WorkArea coords.Rect `json:"workArea"`
Modes []*DisplayMode `json:"modes"`
HasTouchSupport bool `json:"hasTouchSupport"`
AvailableDisplayZoomFactors []float64 `json:"availableDisplayZoomFactors"`
DisplayZoomFactor float64 `json:"displayZoomFactor"`
}
// GetSelectedMode returns the currently selected display mode. It returns
// nil if no such mode is found.
func (info *Info) GetSelectedMode() (*DisplayMode, error) {
for _, mode := range info.Modes {
if mode.IsSelected {
return mode, nil
}
}
return nil, errors.New("no modes are selected")
}
// GetEffectiveDeviceScaleFactor computes the ratio of a DIP (device independent
// pixel) to a physical pixel, which is DisplayZoomFactor x DeviceScaleFactor.
// See also ui/display/manager/managed_display_info.h in Chromium.
func (info *Info) GetEffectiveDeviceScaleFactor() (float64, error) {
mode, err := info.GetSelectedMode()
if err != nil {
return 0, err
}
scaleFactor := info.DisplayZoomFactor * mode.DeviceScaleFactor
// Make sure the scale factor is neither 0 nor NaN.
if math.IsNaN(scaleFactor) || math.Abs(scaleFactor) < 1e-10 {
return 0, errors.Errorf("invalid device scale factor: %f", scaleFactor)
}
return scaleFactor, nil
}
// GetInfo calls chrome.system.display.getInfo to get information about connected displays.
// See https://developer.chrome.com/docs/extensions/reference/system_display/#method-getInfo.
func GetInfo(ctx context.Context, tconn *chrome.TestConn) ([]Info, error) {
var infos []Info
if err := tconn.Call(ctx, &infos, `tast.promisify(chrome.system.display.getInfo)`); err != nil {
return nil, errors.Wrap(err, "failed to get display info")
}
if len(infos) == 0 {
// At leasat one display info should exist. So empty info would mean
// something is wrong.
return nil, errors.New("no display info are contained")
}
return infos, nil
}
// FindInfo returns information about the display that satisfies the given predicate.
func FindInfo(ctx context.Context, tconn *chrome.TestConn, predicate func(info *Info) bool) (*Info, error) {
infos, err := GetInfo(ctx, tconn)
if err != nil {
return nil, err
}
for _, info := range infos {
if predicate(&info) {
return &info, nil
}
}
return nil, errors.New("failed to find a display satisfying the condition")
}
// GetInternalInfo returns information about the internal display.
// An error is returned if no internal display is present.
func GetInternalInfo(ctx context.Context, tconn *chrome.TestConn) (*Info, error) {
return FindInfo(ctx, tconn, func(info *Info) bool {
return info.IsInternal
})
}
// GetPrimaryInfo returns information about the primary display.
func GetPrimaryInfo(ctx context.Context, tconn *chrome.TestConn) (*Info, error) {
return FindInfo(ctx, tconn, func(info *Info) bool {
return info.IsPrimary
})
}
// DisplayProperties holds properties to change and is passed to SetDisplayProperties.
// nil fields are ignored. See https://developer.chrome.com/docs/extensions/reference/system_display/#type-DisplayProperties.
type DisplayProperties struct { // NOLINT
IsUnified *bool `json:"isUnified,omitempty"`
MirroringSourceID *string `json:"mirroringSourceId,omitempty"`
IsPrimary *bool `json:"isPrimary,omitempty"`
Overscan *Insets `json:"overscan,omitempty"`
Rotation *int `json:"rotation,omitempty"`
BoundsOriginX *int `json:"boundsOriginX,omitempty"`
BoundsOriginY *int `json:"boundsOriginY,omitempty"`
DisplayMode *DisplayMode `json:"displayMode,omitempty"`
DisplayZoomFactor *float64 `json:"displayZoomFactor,omitempty"`
}
// SetDisplayProperties updates the properties for the display specified by id.
// See https://developer.chrome.com/docs/extensions/reference/system_display/#method-setDisplayProperties.
// Some properties, like rotation, will be performed in an async way. For rotation in particular,
// you should call display.WaitForDisplayRotation() to know when the rotation animation finishes.
func SetDisplayProperties(ctx context.Context, tconn *chrome.TestConn, id string, dp DisplayProperties) error {
return tconn.Call(ctx, nil, "tast.promisify(chrome.system.display.setDisplayProperties)", id, dp)
}
// RotationAngle represents the supported rotation angles by SetDisplayRotationSync.
type RotationAngle string
// Rotation values as defined in: https://cs.chromium.org/chromium/src/out/Debug/gen/chrome/common/extensions/api/autotest_private.h
const (
// RotateAny represents the auto-rotation to the device angle. Valid only in
// tablet mode.
RotateAny RotationAngle = "RotateAny"
// Rotate0 represents rotation angle 0.
Rotate0 RotationAngle = "Rotate0"
// Rotate90 represents rotation angle 90.
Rotate90 RotationAngle = "Rotate90"
// Rotate180 represents rotation angle 180.
Rotate180 RotationAngle = "Rotate180"
// Rotate270 represents rotation angle 270.
Rotate270 RotationAngle = "Rotate270"
)
// WaitForDisplayRotation waits for the display rotation animation. If it is not
// animating, it returns immediately. Returns an error if incorrect parameters
// are passed, or the display rotation ends up with a different rotation from
// the specified one.
func WaitForDisplayRotation(ctx context.Context, tconn *chrome.TestConn, dispID string, rot RotationAngle) error {
return tconn.Call(ctx, nil, `async (displayId, rotation) => {
if (!await tast.promisify(chrome.autotestPrivate.waitForDisplayRotation)(displayId, rotation))
throw new Error("failed to wait for display rotation");
}`, dispID, rot)
}
// SetDisplayRotationSync rotates the display to a certain angle and waits until the rotation animation finished.
// c must be a connection with both system.display and autotestPrivate permissions.
func SetDisplayRotationSync(ctx context.Context, tconn *chrome.TestConn, dispID string, rot RotationAngle) error {
var rotInt int
switch rot {
case RotateAny:
rotInt = -1
case Rotate0:
rotInt = 0
case Rotate90:
rotInt = 90
case Rotate180:
rotInt = 180
case Rotate270:
rotInt = 270
default:
return errors.Errorf("unexpected rotation value; got %q, want: any of [Rotate0,Rotate90,Rotate180,Rotate270]", rot)
}
p := DisplayProperties{Rotation: &rotInt}
if err := SetDisplayProperties(ctx, tconn, dispID, p); err != nil {
return errors.Wrapf(err, "failed to set rotation to %d", rotInt)
}
return WaitForDisplayRotation(ctx, tconn, dispID, rot)
}
// OrientationType represents a display orientation.
type OrientationType string
// OrientationType values as "enum OrientationType" defined in https://w3c.github.io/screen-orientation/#screenorientation-interface
const (
OrientationPortraitPrimary OrientationType = "portrait-primary"
OrientationPortraitSecondary OrientationType = "portrait-secondary"
OrientationLandscapePrimary OrientationType = "landscape-primary"
OrientationLandscapeSecondary OrientationType = "landscape-secondary"
)
// Orientation holds information obtained from the screen orientation API.
// See https://w3c.github.io/screen-orientation/#screenorientation-interface
type Orientation struct {
// Angle is an angle in degrees of the display counterclockwise from the
// orientation of the display panel.
Angle int `json:"angle"`
// Type is an OrientationType representing the display orientation.
Type OrientationType `json:"type"`
}
// GetOrientation returns the Orientation of the display.
func GetOrientation(ctx context.Context, tconn *chrome.TestConn) (*Orientation, error) {
result := &Orientation{}
// Using a JS expression to evaluate screen.orientation to a JSON object
// because JSON.stringify does not work for it and returns {}.
if err := tconn.Eval(ctx, `s=screen.orientation;o={"angle":s.angle,"type":s.type}`, result); err != nil {
return nil, err
}
return result, nil
}
// IsFakeDisplayID checks if a display is fake or not by its id.
func IsFakeDisplayID(id string) bool {
// the id of fake displays will start from this number.
// See also: https://source.chromium.org/chromium/chromium/src/+/HEAD:ui/display/manager/managed_display_info.cc?q=%20kSynthesizedDisplayIdStart
const fakeDisplayID = "2200000000"
// Theoretically it is possible that a fake display has a different ID. This
// happens when some displays are connected and then disconnected; this is
// unlikely to happen on test environment.
return id == fakeDisplayID
}
// PhysicalDisplayConnected checks the display info and returns true if at least
// one physical display is connected.
func PhysicalDisplayConnected(ctx context.Context, tconn *chrome.TestConn) (bool, error) {
infos, err := GetInfo(ctx, tconn)
if err != nil {
return false, err
}
if len(infos) > 1 {
return true, nil
}
return !IsFakeDisplayID(infos[0].ID), nil
}
var intToRotationAngle = map[int]RotationAngle{
0: Rotate0,
90: Rotate90,
180: Rotate180,
270: Rotate270,
-1: RotateAny,
}
// RotateToLandscape rotates the display only if current orientation type is "portrait", and returns
// a function that restores the original orientation setting.
func RotateToLandscape(ctx context.Context, tconn *chrome.TestConn) (func(context.Context) error, error) {
orientation, err := GetOrientation(ctx, tconn)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain the orientation info")
}
testing.ContextLogf(ctx, "Current orientation type: %d; orientation angle: %v", orientation.Angle, orientation.Type)
// Rotate the display only if current orientation is in portrait position.
if orientation.Type == OrientationPortraitPrimary || orientation.Type == OrientationPortraitSecondary {
info, err := GetPrimaryInfo(ctx, tconn)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain primary display info")
}
restoreRotation := intToRotationAngle[info.Rotation]
testing.ContextLog(ctx, "Current rotation setting: ", restoreRotation)
// If the orientation angle is equal to 0 or 180, rotate 270 (or 90) degrees.
// If the orientation angle is equal to 90 or 270, rotate 0 (or 180) degrees.
var targetRotation RotationAngle
if orientation.Angle == 0 || orientation.Angle == 180 {
targetRotation = Rotate270
} else {
targetRotation = Rotate0
}
testing.ContextLog(ctx, "Target rotation setting: ", targetRotation)
if err := SetDisplayRotationSync(ctx, tconn, info.ID, targetRotation); err != nil {
return nil, err
}
return func(ctx context.Context) error {
return SetDisplayRotationSync(ctx, tconn, info.ID, restoreRotation)
}, nil
}
return func(context.Context) error {
return nil
}, nil
}