blob: 06e2a573747174e20883e33651f421ba791e4309 [file] [log] [blame]
// Copyright 2021 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 phonehub
import (
"context"
"crypto/sha256"
"encoding/hex"
"io"
"os"
"path/filepath"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome/crossdevice"
"chromiumos/tast/local/chrome/crossdevice/phonehub"
"chromiumos/tast/local/chrome/uiauto"
"chromiumos/tast/local/chrome/uiauto/faillog"
"chromiumos/tast/local/chrome/uiauto/filesapp"
"chromiumos/tast/local/chrome/uiauto/holdingspace"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: DownloadRecentPhoto,
LacrosStatus: testing.LacrosVariantUnneeded,
Desc: "Exercises toggling the Recent Photos feature and downloading a photo from a connected phone",
Contacts: []string{
"jasonsun@chromium.org",
"chromeos-sw-engprod@google.com",
"chromeos-cross-device-eng@google.com",
},
Attr: []string{"group:cross-device"},
SoftwareDeps: []string{"chrome"},
Fixture: "crossdeviceOnboardedAllFeatures",
})
}
// DownloadRecentPhoto exercises toggling the Recent Photos feature and downloading a photo from a connected phone.
func DownloadRecentPhoto(ctx context.Context, s *testing.State) {
androidDevice := s.FixtValue().(*crossdevice.FixtData).AndroidDevice
chrome := s.FixtValue().(*crossdevice.FixtData).Chrome
tconn := s.FixtValue().(*crossdevice.FixtData).TestConn
defer faillog.DumpUITreeOnError(ctx, s.OutDir(), s.HasError, tconn)
ui := uiauto.New(tconn)
photoName, err := androidDevice.TakePhoto(ctx)
if err != nil {
s.Fatal("Failed to take a photo on the Android phone: ", err)
}
androidFilePath := filepath.Join(crossdevice.AndroidPhotosPath, photoName)
defer androidDevice.RemoveMediaFile(ctx, androidFilePath)
// Open Phone Hub and enable Recent Photos via the opt-in view.
if err := phonehub.Show(ctx, tconn); err != nil {
s.Fatal("Failed to open Phone Hub: ", err)
}
if err := phonehub.OptInRecentPhotos(ctx, tconn, chrome); err != nil {
s.Fatal("Failed to enable Recent Photos via the opt-in view: ", err)
}
if err := androidDevice.TurnOnRecentPhotosFeature(ctx); err != nil {
s.Fatal("Failed to enable Recent Photos on the phone: ", err)
}
// Download the newly taken photo to Tote.
if err := phonehub.DownloadMostRecentPhoto(ctx, tconn); err != nil {
s.Fatal("Failed to download the most recent photo: ", err)
}
if err := uiauto.Combine("view downloaded photo in the holding space tray",
ui.LeftClick(holdingspace.FindTray()),
ui.Exists(holdingspace.FindDownloadChip().Name(photoName).First()),
)(ctx); err != nil {
s.Fatal("Expected photo ", photoName, " is not displayed in the holding space tray: ", err)
}
// Verify the downloaded file content.
sourceFileSizeBytes, err := androidDevice.FileSize(ctx, androidFilePath)
if err != nil {
s.Fatal("Failed to read source photo size: ", err)
}
if err := waitUntilDownloadComplete(ctx, photoName, sourceFileSizeBytes); err != nil {
s.Fatal("Photo download cannot be completed: ", err)
}
if err := comparePhotoHashes(ctx, photoName, androidDevice); err != nil {
s.Fatal("Failed to verify hash of the downloaded photo: ", err)
}
// Hide Phone Hub and disable Recent Photos from the Settings page.
if err := phonehub.Hide(ctx, tconn); err != nil {
s.Fatal("Failed to open Phone Hub: ", err)
}
if err := phonehub.ToggleRecentPhotosSetting(ctx, tconn, chrome, false); err != nil {
s.Fatal("Failed to disable Recent Photos: ", err)
}
}
// waitUntilDownloadComplete waits for the target photo to be fully downloaded to the CrOS device's download directory.
func waitUntilDownloadComplete(ctx context.Context, photoName string, sourceFileSizeBytes int64) error {
crosFilePath := filepath.Join(filesapp.DownloadPath, photoName)
if err := testing.Poll(ctx, func(ctx context.Context) error {
fi, err := os.Stat(crosFilePath)
if err != nil {
return testing.PollBreak(errors.Wrap(err, "failed to get the size of the downloaded photo on CrOS"))
}
if currentSizeBytes := fi.Size(); currentSizeBytes != sourceFileSizeBytes {
return errors.Errorf("Photo download not complete yet: expected %d bytes, downloaded %d bytes", sourceFileSizeBytes, currentSizeBytes)
}
return nil
}, &testing.PollOptions{Timeout: time.Minute}); err != nil {
return errors.Wrap(err, "failed to wait for photo download to complete")
}
return nil
}
// comparePhotoHashes verifies that the hash of the downloaded photo matches the hash of the source photo on the Android device.
func comparePhotoHashes(ctx context.Context, photoName string, androidDevice *crossdevice.AndroidDevice) error {
androidFilePath := filepath.Join(crossdevice.AndroidPhotosPath, photoName)
androidHash, err := androidDevice.SHA256Sum(ctx, androidFilePath)
if err != nil {
return errors.Wrap(err, "failed to compute hash of the source photo on the Android device")
}
crosFilePath := filepath.Join(filesapp.DownloadPath, photoName)
crosHash, err := hashFile(ctx, crosFilePath)
if err != nil {
return errors.Wrap(err, "failed to compute hash of the downloaded photo on the CrOS device")
}
if androidHash != crosHash {
return errors.Errorf("Hash mismatch for downloaded photo %s: expected=%s, actual=%s", photoName, androidHash, crosHash)
}
return nil
}
// hashFile computes the SHA256 checksum of the given file.
func hashFile(ctx context.Context, filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", errors.Wrapf(err, "failed to open file %s", filePath)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", errors.Wrapf(err, "failed to copy %s file contents to the hasher", filePath)
}
return hex.EncodeToString(h.Sum(nil)), nil
}