blob: 9fba79015db71d760a8dbc31eae9746ff1e03716 [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 ash
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/png"
"io/ioutil"
"os"
"path/filepath"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/internal/cdputil"
"chromiumos/tast/local/chrome/internal/extension"
)
// AppListBubbleClassName is the automation API class name of the bubble launcher.
const AppListBubbleClassName = "AppListBubbleView"
// LauncherState represents the launcher (a.k.a AppList) state.
type LauncherState string
// LauncherState as defined in
// https://cs.chromium.org/chromium/src/ash/public/cpp/app_list/app_list_types.h
const (
Peeking LauncherState = "Peeking"
FullscreenAllApps LauncherState = "FullscreenAllApps"
FullscreenSearch LauncherState = "FullscreenSearch"
Half LauncherState = "Half"
Closed LauncherState = "Closed"
)
// Accelerator represents the accelerator key to trigger certain actions.
type Accelerator struct {
KeyCode string `json:"keyCode"`
Shift bool `json:"shift"`
Control bool `json:"control"`
Alt bool `json:"alt"`
Search bool `json:"search"`
}
// Accelerator key used to trigger launcher state change.
var (
AccelSearch = Accelerator{KeyCode: "search", Shift: false, Control: false, Alt: false, Search: false}
AccelShiftSearch = Accelerator{KeyCode: "search", Shift: true, Control: false, Alt: false, Search: false}
)
// WaitForLauncherState waits until the launcher state becomes state. It waits
// up to 10 seconds and fail if the launcher doesn't have the desired state.
// Expected to fail with "Not supported for bubble launcher" error when waiting
// for state different from "Closed" if called for clamshell productivity (bubble)
// launcher. Note that the autotest API is expected to return immediately, but still
// asynchronously, in this case.
// NOTE: Waiting for "Closed" state will always wait for the fullscreen launcher to
// hide, even if one would otherwise expect bubble launcher to be used for the current
// session state - this supports waiting for launcher UI hide animation to complete
// after transitioning from tablet mode to clamshell.
func WaitForLauncherState(ctx context.Context, tconn *chrome.TestConn, state LauncherState) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := tconn.Call(ctx, nil, "tast.promisify(chrome.autotestPrivate.waitForLauncherState)", state); err != nil {
return errors.Wrap(err, "failed to wait for launcher state")
}
return nil
}
// TriggerLauncherStateChange will cause the launcher state change via accelerator.
func TriggerLauncherStateChange(ctx context.Context, tconn *chrome.TestConn, accel Accelerator) error {
// Send the press event to store it in the history. It'll not be handled, so ignore the result.
if err := tconn.Call(ctx, nil, `async (acceleratorKey) => {
acceleratorKey.pressed = true;
chrome.autotestPrivate.activateAccelerator(acceleratorKey, () => {});
acceleratorKey.pressed = false;
await tast.promisify(chrome.autotestPrivate.activateAccelerator)(acceleratorKey);
}`, accel); err != nil {
return errors.Wrap(err, "failed to execute accelerator")
}
return nil
}
func scaleImage(src image.Image, siz int) image.Image {
srcSize := src.Bounds().Size().X
scaled := image.NewRGBA(image.Rect(0, 0, siz, siz))
for x := 0; x < siz; x++ {
for y := 0; y < siz; y++ {
scaled.Set(x, y, src.At(x*srcSize/siz, y*srcSize/siz))
}
}
return scaled
}
func saveImageAsPng(filename string, img image.Image) error {
w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer w.Close()
return png.Encode(w, img)
}
// generateFakeAppNames generates default names for fake apps.
func generateFakeAppNames(numFakeApps int) []string {
fakeAppNames := make([]string, numFakeApps)
for i := 0; i < numFakeApps; i++ {
fakeAppNames[i] = fmt.Sprintf("fake app %d", i)
}
return fakeAppNames
}
// GeneratePrepareFakeAppsWithNamesOptions calls PrepareDefaultFakeApps() and
// returns options to be used by chrome.New() for logging in with the newly
// created fake apps. baseDir is the path to the directory for keeping app data.
// The function caller should always clean baseDir regardless of function
// execution results. names specify app names.
func GeneratePrepareFakeAppsWithNamesOptions(baseDir string, names []string) ([]chrome.Option, error) {
dirs, err := PrepareDefaultFakeApps(baseDir, names, true)
if err != nil {
return nil, errors.Wrap(err, "failed to create fake apps")
}
opts := make([]chrome.Option, 0, len(names))
for _, dir := range dirs {
opts = append(opts, chrome.UnpackedExtension(dir))
}
return opts, nil
}
// GeneratePrepareFakeAppsWithIconDataOptions is similar with GeneratePrepareFakeAppsWithNamesOptions,
// with a difference that GeneratePrepareFakeAppsWithIconDataOptions allows the
// caller to specify both app names and icon data. The caller has the duty to
// clean baseDir.
func GeneratePrepareFakeAppsWithIconDataOptions(baseDir string, names []string, iconData [][]byte) ([]chrome.Option, error) {
if len(names) != len(iconData) {
return nil, errors.Errorf("unexpected count of icon data: got %d, expecting %d", len(iconData), len(names))
}
dirs, err := prepareFakeAppsWithIconData(baseDir, names, iconData)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare data for fake apps")
}
opts := make([]chrome.Option, 0, len(names))
for _, dir := range dirs {
opts = append(opts, chrome.UnpackedExtension(dir))
}
return opts, nil
}
// GeneratePrepareFakeAppsOptions is similar with GeneratePrepareFakeAppsWithNamesOptions,
// with a difference that GeneratePrepareFakeAppsOptions accepts the fake app
// count as the parameter.
func GeneratePrepareFakeAppsOptions(baseDir string, numFakeApps int) ([]chrome.Option, error) {
return GeneratePrepareFakeAppsWithNamesOptions(baseDir, generateFakeAppNames(numFakeApps))
}
// prepareFakeApp creates data for a fake app with the specified app name and
// icon (if any).
func prepareFakeApp(baseDir, appName, iconDir string, iconFileMap map[int]string) (string, error) {
// The manifest.json data for the fake hosted app; it just opens google.com
// page on launch.
const manifestTmpl = `{
"description": "fake",
"name": "%s",
"manifest_version": 2,
"version": "0",
%s
"app": {
"launch": {
"web_url": "https://www.google.com/"
}
}
}`
extDir := filepath.Join(baseDir, appName)
if err := os.Mkdir(extDir, 0755); err != nil {
return "", errors.Wrapf(err, "failed to create the directory for %s", appName)
}
var iconJSON string
if iconDir != "" {
for _, iconFileName := range iconFileMap {
if err := os.Symlink(filepath.Join(iconDir, iconFileName), filepath.Join(extDir, iconFileName)); err != nil {
return "", errors.Wrapf(err, "failed to create link of icon %s", iconFileName)
}
}
iconJSONData, err := json.Marshal(iconFileMap)
if err != nil {
return "", errors.Wrap(err, "failed to turn the mapptings between icon sizes and icon names into a JSON string")
}
iconJSON = fmt.Sprintf(`"icons": %s,`, string(iconJSONData))
}
if err := ioutil.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(fmt.Sprintf(manifestTmpl, appName, iconJSON)), 0644); err != nil {
return "", errors.Wrapf(err, "failed to prepare manifest.json for %s", appName)
}
return extDir, nil
}
// prepareFakeAppIcon creates icon images in different scales with the given
// icon data. These images are stored in a directory created under baseDir.
// iconFolder specifies the directory's name.
func prepareFakeAppIcon(baseDir, iconFolder string, iconData []byte) (string, map[int]string, error) {
iconDir := filepath.Join(baseDir, iconFolder)
if err := os.Mkdir(iconDir, 0755); err != nil {
return "", nil, errors.Wrapf(err, "failed to create the icon directory %q", iconDir)
}
img, err := png.Decode(bytes.NewReader(iconData))
if err != nil {
return "", nil, errors.Wrap(err, "failed to decode icon data")
}
iconFiles := map[int]string{}
for _, siz := range []int{32, 48, 64, 96, 128, 192} {
var imgToSave image.Image
if siz == img.Bounds().Size().X {
imgToSave = img
} else {
imgToSave = scaleImage(img, siz)
}
iconFile := fmt.Sprintf("icon%d.png", siz)
iconFileFullPath := filepath.Join(iconDir, iconFile)
if err := saveImageAsPng(iconFileFullPath, imgToSave); err != nil {
return "", nil, errors.Wrapf(err, "failed to save the icon file to %q", iconFileFullPath)
}
iconFiles[siz] = iconFile
}
return iconDir, iconFiles, nil
}
// PrepareDefaultFakeApps creates directories for fake apps (hosted apps) under
// the directory of baseDir and returns their path names. Fake app names are
// specified by the parameter. hasIcon specifies whether a default icon should
// be used. The intermediate data may remain even when an error is returned. It
// is the caller's responsibility to clean up the contents under the baseDir.
// This also may update the ownership of baseDir.
func PrepareDefaultFakeApps(baseDir string, appNames []string, hasIcon bool) ([]string, error) {
if err := extension.ChownContentsToChrome(baseDir); err != nil {
return nil, errors.Wrapf(err, "failed to change ownership of %q", baseDir)
}
var iconDir string
var iconFiles map[int]string
var err error
if hasIcon {
iconDir, iconFiles, err = prepareFakeAppIcon(baseDir, "defaultIcons", fakeIconData)
if err != nil {
return nil, errors.Wrap(err, "failed to parepare the shared icon for fake apps")
}
}
var dirs []string
for _, appName := range appNames {
dir, err := prepareFakeApp(baseDir, appName, iconDir, iconFiles)
if err != nil {
return nil, errors.Wrapf(err, "failed to prepare data for %q", appName)
}
dirs = append(dirs, dir)
}
return dirs, nil
}
// prepareFakeAppsWithIconData is similar with PrepareDefaultFakeApps, but with
// the difference that app icons are specified by the parameter.
func prepareFakeAppsWithIconData(baseDir string, appNames []string, iconData [][]byte) ([]string, error) {
if len(appNames) != len(iconData) {
return nil, errors.Errorf("unexpected count of icon data: got %d, expecting %d", len(iconData), len(appNames))
}
if err := extension.ChownContentsToChrome(baseDir); err != nil {
return nil, errors.Wrapf(err, "failed to change ownership of %q", baseDir)
}
var dirs []string
for index, appName := range appNames {
iconDir, iconFiles, err := prepareFakeAppIcon(baseDir, appName+"Icons", iconData[index])
if err != nil {
return nil, errors.Wrapf(err, "failed to parepare icons for the fake app %q", appName)
}
dir, err := prepareFakeApp(baseDir, appName, iconDir, iconFiles)
if err != nil {
return nil, errors.Wrapf(err, "failed to prepare data for %q", appName)
}
dirs = append(dirs, dir)
}
return dirs, nil
}
// The remaining definitions are needed only for faillog & CaptureCDP.
// TODO(crbug.com/1271473): Get rid of them.
// They expose cdputil types and values. See the cdputil package for details.
// DebuggingPortPath is a file where Chrome writes debugging port.
const DebuggingPortPath = cdputil.DebuggingPortPath
// DevtoolsConn is the connection to a web content view, e.g. a tab.
type DevtoolsConn = cdputil.Conn
// Session maintains the connection to talk to the browser in Chrome DevTools Protocol
// over WebSocket.
type Session = cdputil.Session
// PortWaitOption controls whether the NewSession should wait for the port file
// to be created.
type PortWaitOption = cdputil.PortWaitOption
// PortWaitOption values.
const (
NoWaitPort PortWaitOption = cdputil.NoWaitPort
WaitPort PortWaitOption = cdputil.WaitPort
)
// NewDevtoolsSession establishes a Chrome DevTools Protocol WebSocket connection to the browser.
func NewDevtoolsSession(ctx context.Context, debuggingPortPath string, portWait PortWaitOption) (sess *Session, retErr error) {
return cdputil.NewSession(ctx, debuggingPortPath, portWait)
}