blob: ae3bdef3fe81722cb57f46b549c7de22e04c904d [file]
// 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 gca
import (
"context"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"time"
"chromiumos/tast/common/android/adb"
"chromiumos/tast/common/android/ui"
"chromiumos/tast/common/testexec"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/cryptohome"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
const (
// Apk is the name of the apk of GoogleCameraArc (GCA).
Apk = "GoogleCameraArc.apk"
// GoogleCameraArc (GCA) package.
pkg = "com.google.android.GoogleCameraArc"
intent = "com.android.camera.CameraLauncher"
idBase = pkg + ":id/"
shutterButtonID = idBase + "shutter_button"
// Predefined timeouts for ease of scaling.
// shortTimeout is a shorter timeout for operations that should complete quickly.
shortTimeout = 5 * time.Second
// longTimeout is a longer timeout for operations that takes longer.
longTimeout = 10 * time.Second
)
var (
// ImagePattern is the filename format of images taken by GCA.
// See: google3/java/com/google/android/apps/chromeos/camera/storage/filenamer/FileNamerModule.java
ImagePattern = regexp.MustCompile(`^IMG_\d{8}_\d{6}(?:_BURST)?(?:_COVER)?\.\w+$`)
// VideoPattern is the filename format of videos taken by GCA.
// See: google3/java/com/google/android/apps/chromeos/camera/storage/filenamer/FileNamerModule.java
VideoPattern = regexp.MustCompile(`^VID_\d{8}_\d{6}\.\w+$`)
)
// TestFunc is the body of a test and is called after the test environment is setup.
type TestFunc func(context.Context, *arc.ARC, *ui.Device)
// Mode refers to the capture mode GCA is in.
type Mode int
const (
// PhotoMode refers to photo mode, in which users can take a photo.
PhotoMode Mode = iota
// VideoMode refers to video mode, in which users can record a video.
VideoMode
)
// Facing refers to the direction a camera is facing.
type Facing int
const (
// Back means the camera is back-facing.
Back Facing = iota
// Front means the camera is front-facing.
Front
// External means the camera is an external USB camera.
External
)
// TimerOption refers to the setting of the countdown timer for capturing photos.
type TimerOption int
const (
// NoTimer option disables the timer.
NoTimer TimerOption = iota
// ThreeSecondTimer sets the timer to 3 seconds.
ThreeSecondTimer
// TenSecondTimer sets the timer to 10 seconds.
TenSecondTimer
)
func (facing Facing) String() string {
switch facing {
case Back:
return "Back"
case Front:
return "Front"
default:
return "External"
}
}
// GetFacing returns the direction the current camera is facing.
func GetFacing(ctx context.Context, d *ui.Device) (Facing, error) {
const viewfinderID = idBase + "viewfinder_frame"
viewfinder := d.Object(ui.ID(viewfinderID))
if err := viewfinder.WaitForExists(ctx, shortTimeout); err != nil {
return External, errors.Wrap(err, "failed to find viewfinder frame (did GCA crash?)")
}
var facing string
if err := testing.Poll(ctx, func(ctx context.Context) error {
camInfo, err := viewfinder.GetContentDescription(ctx)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to get content description of viewfinder frame"))
}
s := strings.Split(camInfo, "|")
if len(s) <= 1 {
return errors.Errorf("failed to read camera info from viewfinder frame (read info %q)", camInfo)
}
facing = s[1]
return nil
}, &testing.PollOptions{Timeout: shortTimeout}); err != nil {
return External, errors.Wrap(err, "timed out reading camera info")
}
switch facing {
case "BACK":
return Back, nil
case "FRONT":
return Front, nil
case "EXTERNAL":
return External, nil
default:
return External, errors.Errorf("unknown camera direction %q", facing)
}
}
// SwitchMode synchronously switches the current mode of GCA to the specified mode.
func SwitchMode(ctx context.Context, d *ui.Device, mode Mode) error {
var switchButtonID string
switch mode {
case PhotoMode:
switchButtonID = idBase + "photo_switch_button"
case VideoMode:
switchButtonID = idBase + "video_switch_button"
}
switchButton := d.Object(ui.ID(switchButtonID), ui.Clickable(true))
if err := switchButton.WaitForExists(ctx, shortTimeout); err != nil {
testing.ContextLog(ctx, "GCA is already in the mode the test wants to switch to")
return nil
}
if err := switchButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click mode switch button")
}
// Wait until the switch button is gone (the other switch button would appear). This indicates the mode switch is done.
if err := d.Object(ui.ID(switchButtonID)).WaitUntilGone(ctx, longTimeout); err != nil {
return errors.Wrap(err, "failed to switch mode")
}
return nil
}
// SwitchCamera synchronously switches the app to the next camera.
func SwitchCamera(ctx context.Context, d *ui.Device) error {
const switchButtonID = idBase + "camera_switch_button"
switchButton := d.Object(ui.ID(switchButtonID), ui.Clickable(true))
if err := switchButton.WaitForExists(ctx, shortTimeout); err != nil {
testing.ContextLog(ctx, "Failed to find camera switch button")
// We might not have a camera switch button if the device only has one camera.
return nil
}
if err := switchButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click camera switch button")
}
// Wait until buttons are clickable again (buttons won't be clickable until preview has successfully started)
if err := switchButton.WaitForExists(ctx, longTimeout); err != nil {
return errors.Wrap(err, "preview failed to start")
}
return nil
}
// SetTimerOption sets the countdown timer to the specified timer option.
func SetTimerOption(ctx context.Context, d *ui.Device, t TimerOption) error {
const (
timerButtonID = idBase + "timer_button"
noTimerButtonID = idBase + "timer_off"
threeSecondTimerButtonID = idBase + "timer_3s"
tenSecondTimerButtonID = idBase + "timer_10s"
)
timerButton := d.Object(ui.ID(timerButtonID), ui.Clickable(true))
if err := timerButton.WaitForExists(ctx, shortTimeout); err != nil {
return errors.Wrap(err, "failed to find timer button")
}
// Expand timer option menu.
if err := timerButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click timer button")
}
// Find and click the corresponding timer option.
var timerOptionButtonID string
switch t {
case NoTimer:
timerOptionButtonID = noTimerButtonID
case ThreeSecondTimer:
timerOptionButtonID = threeSecondTimerButtonID
case TenSecondTimer:
timerOptionButtonID = tenSecondTimerButtonID
}
timerOptionButton := d.Object(ui.ID(timerOptionButtonID), ui.Clickable(true))
if err := timerOptionButton.WaitForExists(ctx, shortTimeout); err != nil {
return errors.Wrap(err, "failed to find the specified timer option")
}
if err := timerOptionButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click timer option")
}
// Wait until the timer option is set.
if err := d.WaitForIdle(ctx, shortTimeout); err != nil {
return errors.Wrap(err, "failed to select specified timer option")
}
return nil
}
// ClickShutterButton clicks the shutter button on the screen. This can be a regular photo shutter button or a recording button.
func ClickShutterButton(ctx context.Context, d *ui.Device) error {
shutterButton := d.Object(ui.ID(shutterButtonID), ui.Clickable(true))
if err := shutterButton.WaitForExists(ctx, shortTimeout); err != nil {
return errors.Wrap(err, "failed to find shutter button")
}
// Click the shutter button.
if err := shutterButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click shutter button")
}
// Wait until the shutter button is clickable again. This indicates that the previous capture is done.
if err := shutterButton.WaitForExists(ctx, shortTimeout); err != nil {
return errors.Wrap(err, "capture couldn't be completed successfully")
}
return nil
}
// VerifyFile examines the Downloads directory for any new files (modification time after specified timestamp) that match the specified pattern.
func VerifyFile(ctx context.Context, cr *chrome.Chrome, pat *regexp.Regexp, ts time.Time) error {
// Get the Downloads directory where we save our media files.
path, err := cryptohome.UserPath(ctx, cr.NormalizedUser())
if err != nil {
return errors.Wrap(err, "failed to get user path")
}
path = filepath.Join(path, "Downloads")
testing.ContextLog(ctx, "Looking for output file")
if err := testing.Poll(ctx, func(ctx context.Context) error {
files, err := ioutil.ReadDir(path)
if err != nil {
return errors.Wrap(err, "failed to read the directory for saving media files")
}
for _, file := range files {
if file.ModTime().Before(ts) {
continue
}
testing.ContextLog(ctx, "New file found: ", file.Name())
if pat.MatchString(file.Name()) {
testing.ContextLog(ctx, "Found a match: ", file.Name())
return nil
}
}
return errors.New("no matching output file found")
}, &testing.PollOptions{Timeout: longTimeout}); err != nil {
return errors.Wrapf(err, "no matching output file found after %v", longTimeout)
}
return nil
}
// RestartApp restarts GCA.
func RestartApp(ctx context.Context, a *arc.ARC, d *ui.Device) error {
if err := a.Command(ctx, "am", "force-stop", pkg).Run(testexec.DumpLogOnError); err != nil {
return errors.Wrap(err, "failed to stop GCA")
}
if err := a.Command(ctx, "am", "start", "-W", "-n", pkg+"/"+intent).Run(testexec.DumpLogOnError); err != nil {
return errors.Wrap(err, "failed to start GCA")
}
if err := d.WaitForIdle(ctx, longTimeout); err != nil {
return errors.Wrap(err, "failed to wait for app to become idle while loading app")
}
return nil
}
// setUpDevice sets up the test environment, including starting UIAutomator server and launching GCA.
func setUpDevice(ctx context.Context, a *arc.ARC, apkPath string) (*ui.Device, error) {
success := false
var d *ui.Device
defer func() {
if !success {
tearDownDevice(ctx, a, d)
}
}()
// The cros-camera job exists only on boards that use the new camera stack.
if upstart.JobExists(ctx, "cros-camera") {
// Restart cros-camera. Camera tests sometimes leave Android camera manager
// or the camera service in a bad state. We restart cros-camera to ensure
// that we always start with a clean state.
if err := upstart.RestartJob(ctx, "cros-camera"); err != nil {
return nil, errors.Wrap(err, "failed to restart cros-camera")
}
}
d, err := a.NewUIDevice(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize UIAutomator")
}
testing.ContextLog(ctx, "Installing app")
if err := a.Install(ctx, apkPath, adb.InstallOptionGrantPermissions); err != nil {
return nil, errors.Wrap(err, "failed to install app")
}
// Launch GCA.
if err := a.Command(ctx, "am", "start", "-W", "-n", pkg+"/"+intent).Run(testexec.DumpLogOnError); err != nil {
return nil, errors.Wrap(err, "failed to start GCA")
}
if err := d.WaitForIdle(ctx, longTimeout); err != nil {
return nil, errors.Wrap(err, "failed to wait for app to become idle while loading app")
}
success = true
return d, nil
}
// tearDownDevice tears down the test environment, including closing GCA and UIAutomator server.
func tearDownDevice(ctx context.Context, a *arc.ARC, d *ui.Device) error {
if d == nil {
// Nothing to shutdown since UIAutomator server isn't even up.
return nil
}
var firstErr error
// Close GCA.
if err := a.Command(ctx, "am", "force-stop", pkg).Run(testexec.DumpLogOnError); err != nil {
err = errors.Wrap(err, "failed to close GCA")
testing.ContextLog(ctx, "Error during teardown: ", err)
if firstErr == nil {
firstErr = err
}
}
// Close UIAutomator server.
// TODO(lnishan): Check the error. Currently Close() always returns an error.
d.Close(ctx)
return firstErr
}
// RunTest sets up the device, runs the specified test and tears down the device afterwards.
func RunTest(ctx context.Context, s *testing.State, f TestFunc) {
// Setup device.
a := s.PreValue().(arc.PreData).ARC
d, err := setUpDevice(ctx, a, s.DataPath(Apk))
if err != nil {
s.Fatal("Failed to set up device: ", err)
}
defer func() {
if err := tearDownDevice(ctx, a, d); err != nil {
s.Error("Failed to tear down device: ", err)
}
}()
// Shorten the context to save time for the teardown procedures.
shortCtx, cancel := ctxutil.Shorten(ctx, longTimeout)
defer cancel()
// Run the test.
f(shortCtx, a, d)
}