blob: 2134fe3719bf058fbb6f8e071ce9d9c5487ea2ce [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 nearbysnippet is for interacting with the Nearby Snippet which provides automated control of Android Nearby share.
package nearbysnippet
import (
"context"
"encoding/json"
"path/filepath"
"strings"
"time"
"chromiumos/tast/common/android"
"chromiumos/tast/common/android/adb"
"chromiumos/tast/common/android/mobly"
"chromiumos/tast/common/android/ui"
nearbycommon "chromiumos/tast/common/cros/nearbyshare"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome/crossdevice"
"chromiumos/tast/testing"
)
// AndroidNearbyDevice represents a connected Android device equipped with Nearby Share controls.
// Nearby Share control is achieved by making RPCs to the Nearby Snippet running on the Android device.
type AndroidNearbyDevice struct {
device *adb.Device
snippetClient *mobly.SnippetClient
transferCallback string
uiDevice *ui.Device
}
// New initializes the specified Android device for Nearby Sharing by setting up the Nearby snippet on the device
// and initializing a Mobly snippet client to communicate with it.
// Callers should defer Cleanup to ensure the resources used by the AndroidNearbyDevice are freed.
func New(ctx context.Context, d *adb.Device, apkZipPath string, overrideGMS bool) (a *AndroidNearbyDevice, err error) {
a = &AndroidNearbyDevice{device: d}
// Override the necessary GMS Core flags.
if overrideGMS {
if err := overrideGMSCoreFlags(ctx, a.device); err != nil {
return a, err
}
}
// Grant the MANAGE_EXTERNAL_STORAGE permission to the Nearby Snippet if the SDK version is 30+ (Android 11+).
// This is required for the Android sender flow, since the Nearby Snippet sends files from external storage.
const needsStoragePermissionsVersion = 30
var permissions []string
if sdkVersion, err := a.device.SDKVersion(ctx); err != nil {
return a, errors.Wrap(err, "failed to get android sdk version")
} else if sdkVersion >= needsStoragePermissionsVersion {
permissions = append(permissions, "MANAGE_EXTERNAL_STORAGE")
}
// Launch the snippet and create a client.
snippetClient, err := mobly.NewSnippetClient(ctx, a.device, moblyPackage, apkZipPath, ApkName, permissions...)
if err != nil {
return a, errors.Wrap(err, "failed to start the snippet client")
}
a.snippetClient = snippetClient
return a, nil
}
// ReconnectToSnippet restarts a connection to the Nearby Snippet on Android device.
func (a *AndroidNearbyDevice) ReconnectToSnippet(ctx context.Context) error {
return a.snippetClient.ReconnectToSnippet(ctx)
}
// Cleanup stops the Nearby Snippet, removes port forwarding, and closes the TCP connection.
// This should be deferred after calling New to ensure the resources used by the AndroidNearbyDevice are released at the end of tests.
func (a *AndroidNearbyDevice) Cleanup(ctx context.Context) {
a.snippetClient.Cleanup(ctx)
}
// gmsOverrideCmd constructs the shell commands to override the GMS Core flags required by the Nearby Snippet.
func gmsOverrideCmd(ctx context.Context, device *adb.Device, pack, flag string) *testexec.Cmd {
return device.ShellCommand(ctx,
"am", "broadcast", "-a", "com.google.android.gms.phenotype.FLAG_OVERRIDE",
"--es", "package", pack,
"--es", "user", `\*`,
"--esa", "flags", flag,
"--esa", "values", "true",
"--esa", "types", "boolean",
"com.google.android.gms")
}
// overrideGMSCoreFlags overrides the GMS Core flags required by the Nearby Snippet.
// Overriding the flags over adb requires the device to be rooted (i.e. userdebug build).
func overrideGMSCoreFlags(ctx context.Context, device *adb.Device) error {
// Get root access.
rootCmd := device.Command(ctx, "root")
if err := rootCmd.Run(); err != nil {
return errors.Wrap(err, "failed to start adb as root")
}
overrideCmd1 := gmsOverrideCmd(ctx, device, "com.google.android.gms.nearby", "sharing_package_whitelist_check_bypass")
overrideCmd2 := gmsOverrideCmd(ctx, device, "com.google.android.gms", "GoogleCertificatesFlags__enable_debug_certificates")
if err := overrideCmd1.Run(); err != nil {
return errors.Wrap(err, "failed to override sharing_package_whitelist_check_bypass flag")
}
if err := overrideCmd2.Run(); err != nil {
return errors.Wrap(err, "failed to override GoogleCertificatesFlags__enable_debug_certificates flag")
}
return nil
}
// DumpLogs saves the Android device's logcat output to a file.
func (a *AndroidNearbyDevice) DumpLogs(ctx context.Context, outDir, filename string) error {
filePath := filepath.Join(outDir, filename)
if err := a.device.DumpLogcat(ctx, filePath); err != nil {
testing.ContextLog(ctx, "Failed to dump Android logs: ", err)
return errors.Wrap(err, "failed to dump Android logs")
}
return nil
}
// ClearLogcat clears logcat so each test run can have only relevant logs.
func (a *AndroidNearbyDevice) ClearLogcat(ctx context.Context) error {
if err := a.device.ClearLogcat(ctx); err != nil {
return errors.Wrap(err, "failed to clear previous logcat logs")
}
return nil
}
// SHA256Sum computes the sha256sum of the specified file on the Android device.
func (a *AndroidNearbyDevice) SHA256Sum(ctx context.Context, filename string) (string, error) {
return a.device.SHA256Sum(ctx, filename)
}
// StageFile pushes the specified file to the Android device to be used in sending.
func (a *AndroidNearbyDevice) StageFile(ctx context.Context, file string) error {
androidDst := filepath.Join(android.DownloadDir, SendDir, filepath.Base(file))
if err := a.device.PushFile(ctx, file, androidDst); err != nil {
return errors.Wrapf(err, "failed to push %v to %v", file, androidDst)
}
return nil
}
// ClearDownloads clears the device's Downloads folder, where outgoing shares are staged and incoming shares are received.
func (a *AndroidNearbyDevice) ClearDownloads(ctx context.Context) error {
if err := a.device.RemoveContents(ctx, android.DownloadDir); err != nil {
return errors.Wrap(err, "failed to clear Android downloads directory")
}
return nil
}
// GetNearbySharingVersion retrieves the Android device's Nearby Sharing version.
func (a *AndroidNearbyDevice) GetNearbySharingVersion(ctx context.Context) (string, error) {
res, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "getNearbySharingVersion")
if err != nil {
return "", err
}
var version string
if err := json.Unmarshal(res.Result, &version); err != nil {
return "", errors.Wrap(err, "failed to parse version number from json result")
}
return version, nil
}
// settingTimeoutSeconds is the time to wait for the Nearby Snippet to return settings values.
// Only used by getDeviceName, getDataUsage, and getVisibility RPCs.
const settingTimeoutSeconds = 10
// GetDeviceName retrieve's the Android device's display name for Nearby Share.
func (a *AndroidNearbyDevice) GetDeviceName(ctx context.Context) (string, error) {
var name string
res, err := a.snippetClient.RPC(ctx, settingTimeoutSeconds*time.Second, "getDeviceName", settingTimeoutSeconds)
if err != nil {
return name, err
}
if err := json.Unmarshal(res.Result, &name); err != nil {
return "", errors.Wrap(err, "failed to parse device name from json result")
}
return name, nil
}
// GetDataUsage retrieve's the Android device's Nearby Share data usage setting.
func (a *AndroidNearbyDevice) GetDataUsage(ctx context.Context) (DataUsage, error) {
var data DataUsage
res, err := a.snippetClient.RPC(ctx, settingTimeoutSeconds*time.Second, "getDataUsage", settingTimeoutSeconds)
if err != nil {
return data, err
}
if err := json.Unmarshal(res.Result, &data); err != nil {
return data, errors.Wrap(err, "failed to parse data usage from json result")
}
return data, nil
}
// GetVisibility retrieve's the Android device's Nearby Share visibility setting.
func (a *AndroidNearbyDevice) GetVisibility(ctx context.Context) (Visibility, error) {
var vis Visibility
res, err := a.snippetClient.RPC(ctx, settingTimeoutSeconds*time.Second, "getVisibility", settingTimeoutSeconds)
if err != nil {
return vis, err
}
if err := json.Unmarshal(res.Result, &vis); err != nil {
return vis, errors.Wrap(err, "failed to parse device visibility from json result")
}
return vis, nil
}
// SetupDevice configures the Android device's Nearby Share settings.
func (a *AndroidNearbyDevice) SetupDevice(ctx context.Context, dataUsage DataUsage, visibility Visibility, name string) error {
_, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "setupDevice", dataUsage, visibility, name)
return err
}
// SetEnabled sets Nearby Share enabled.
func (a *AndroidNearbyDevice) SetEnabled(ctx context.Context, enabled bool) error {
_, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "setEnabled", enabled)
return err
}
// ReceiveFile starts receiving with a timeout.
// Sets the AndroidNearbyDevice's transferCallback, which is needed when awaiting follow-up SnippetEvents when calling eventWaitAndGet.
func (a *AndroidNearbyDevice) ReceiveFile(ctx context.Context, senderName, receiverName string, isHighVisibility bool, turnaroundTime time.Duration) error {
// Reset the transferCallback between shares.
a.transferCallback = ""
res, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "receiveFile", senderName, receiverName, isHighVisibility, int(turnaroundTime.Seconds()))
if err != nil {
return err
}
a.transferCallback = res.Callback
return nil
}
// AwaitReceiverAccept should be used to wait for the onAwaitingReceiverAccept SnippetEvent, which indicates
// that the Android sender has successfully connected to the receiver. The response includes the secure connection token.
func (a *AndroidNearbyDevice) AwaitReceiverAccept(ctx context.Context, timeout time.Duration) (string, error) {
if a.transferCallback == "" {
return "", errors.New("transferCallback is not set, a share needs to be initiated first")
}
res, err := a.snippetClient.EventWaitAndGet(ctx, a.transferCallback, string(SnippetEventOnAwaitingReceiverAccept), timeout)
if err != nil {
return "", errors.Wrap(err, "failed waiting for onAwaitingReceiverAccept event to know that Android sender has connected to receiver")
}
token, ok := res.Data["token"]
if !ok {
return "", errors.New("onAwaitingReceiverAccept event did not include a token")
}
tokenStr, ok := token.(string)
if !ok {
return "", errors.Wrap(err, "share token in onAwaitingReceiverAccept response was not a string")
}
return tokenStr, nil
}
// AwaitReceiverConfirmation should be used after ReceiveFile to wait for the onLocalConfirmation SnippetEvent, which indicates
// that the Android device has detected the incoming share and is awaiting confirmation to begin the transfer.
func (a *AndroidNearbyDevice) AwaitReceiverConfirmation(ctx context.Context, timeout time.Duration) error {
if a.transferCallback == "" {
return errors.New("transferCallback is not set, ReceiveFile should be executed first")
}
if _, err := a.snippetClient.EventWaitAndGet(ctx, a.transferCallback, string(SnippetEventOnLocalConfirmation), timeout); err != nil {
return errors.Wrap(err, "failed waiting for onLocalConfirmation event to know that Android is ready to start the transfer")
}
return nil
}
// AwaitSharingStopped waits for the onStop event, which indicates that sharing has stopped and Android Nearby Share teardown
// tasks have been completed. It does not necessarily indicate that the transfer succeeded.
func (a *AndroidNearbyDevice) AwaitSharingStopped(ctx context.Context, timeout time.Duration) error {
if a.transferCallback == "" {
return errors.New("transferCallback is not set, a share needs to be initiated first")
}
if _, err := a.snippetClient.EventWaitAndGet(ctx, a.transferCallback, string(SnippetEventOnStop), timeout); err != nil {
return errors.Wrap(err, "failed waiting for onStop event to know that transfer is complete on Android")
}
return nil
}
// AcceptTheSharing accepts the share on the receiver side.
func (a *AndroidNearbyDevice) AcceptTheSharing(ctx context.Context, token string) error {
var err error
if token == "" {
// Sometimes there will be no sharing token for in-contact shares.
// In this case, sending nil as the RPC param will make the snippet skip the token verification.
_, err = a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "acceptTheSharing", nil)
} else {
_, err = a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "acceptTheSharing", token)
}
return err
}
// CancelReceivingFile ends Nearby Share on the receiving side. This is used to fail fast instead of waiting for ReceiveFile's timeout.
func (a *AndroidNearbyDevice) CancelReceivingFile(ctx context.Context) error {
_, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "cancelReceivingFile")
return err
}
// SendFile starts sending with a timeout.
// Sets the AndroidNearbyDevice's transferCallback, which is needed when awaiting follow-up SnippetEvents when calling eventWaitAndGet.
func (a *AndroidNearbyDevice) SendFile(ctx context.Context, senderName, receiverName, shareFileName string, mimetype nearbycommon.MimeType, turnaroundTime time.Duration) error {
// Reset the transferCallback between shares.
a.transferCallback = ""
res, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "sendFile", senderName, receiverName, shareFileName, mimetype, int(turnaroundTime.Seconds()))
if err != nil {
return err
}
a.transferCallback = res.Callback
return nil
}
// CancelSendingFile ends Nearby Share on the sending side. This is used to fail fast instead of waiting for SendFile's timeout.
func (a *AndroidNearbyDevice) CancelSendingFile(ctx context.Context) error {
_, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "cancelSendingFile")
return err
}
// Sync synchronizes contact information and certificates on the Android device. This should be used before attempting to receive a contacts share.
func (a *AndroidNearbyDevice) Sync(ctx context.Context) error {
_, err := a.snippetClient.RPC(ctx, mobly.DefaultRPCResponseTimeout, "sync")
return err
}
// InitUI initializes a UI automator connection to the Android device. Callers should defer CloseUI to free the associated resources.
func (a *AndroidNearbyDevice) InitUI(ctx context.Context) error {
d, err := ui.NewDeviceWithRetry(ctx, a.device)
if err != nil {
return errors.Wrap(err, "failed initializing UI automator")
}
a.uiDevice = d
return nil
}
// CloseUI closes the UI automator connection.
func (a *AndroidNearbyDevice) CloseUI(ctx context.Context) error {
return a.uiDevice.Close(ctx)
}
// WaitForInContactSenderUI waits for the sharing UI that appears when there is an incoming share from a contact.
func (a *AndroidNearbyDevice) WaitForInContactSenderUI(ctx context.Context, sender string, timeout time.Duration) error {
senderText := a.uiDevice.Object(ui.ID("com.google.android.gms:id/title"))
return senderText.WaitForText(ctx, sender, timeout)
}
// AcceptUI accepts the incoming contacts share through the UI and waits for the share to finish by waiting for the receiving UI to be gone.
func (a *AndroidNearbyDevice) AcceptUI(ctx context.Context, timeout time.Duration) error {
acceptBtn := a.uiDevice.Object(ui.ID("com.google.android.gms:id/accept_btn"))
if err := acceptBtn.WaitForExists(ctx, 10*time.Second); err != nil {
return errors.Wrap(err, "failed waiting for Accept button to exist")
}
if err := acceptBtn.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click Accept button")
}
receiveCard := a.uiDevice.Object(ui.ID("com.google.android.gms:id/card"))
if err := receiveCard.WaitUntilGone(ctx, timeout); err != nil {
return errors.Wrap(err, "failed waiting for receive UI to be gone")
}
return nil
}
// AndroidAttributes contains information about the Android device and its settings that are relevant to Nearby Share.
// "Android" is redundantly prepended to the field names to make them easy to distinguish from CrOS attributes in test logs.
type AndroidAttributes struct {
BasicAttributes *crossdevice.AndroidAttributes
DisplayName string
DataUsage string
Visibility string
NearbyShareVersion string
NearbyShareChannel string
}
// GetAndroidAttributes returns the AndroidAttributes for the device.
func (a *AndroidNearbyDevice) GetAndroidAttributes(ctx context.Context) (*AndroidAttributes, error) {
// Get the base set of Android attributes used in all crossdevice tests.
basicAttributes, err := crossdevice.GetAndroidAttributes(ctx, a.device)
if err != nil {
return nil, errors.Wrap(err, "failed to get base set of crossdevice Android attributes for reporting")
}
// Add nearby specific attributes.
metadata := AndroidAttributes{
BasicAttributes: basicAttributes,
}
displayName, err := a.GetDeviceName(ctx)
if err != nil {
return nil, err
}
metadata.DisplayName = displayName
dataUsage, err := a.GetDataUsage(ctx)
if err != nil {
return nil, err
}
if val, ok := DataUsageStrings[dataUsage]; ok {
metadata.DataUsage = val
} else {
return nil, errors.Errorf("undefined dataUsage: %v", dataUsage)
}
visibility, err := a.GetVisibility(ctx)
if err != nil {
return nil, err
}
if val, ok := VisibilityStrings[visibility]; ok {
metadata.Visibility = val
} else {
return nil, errors.Errorf("undefined visibility: %v", visibility)
}
nearbyVersion, err := a.GetNearbySharingVersion(ctx)
if err != nil {
return nil, err
}
metadata.NearbyShareVersion = nearbyVersion
nearbyChannel, err := a.device.ShellCommand(ctx, "sh", "-c", `dumpsys activity service com.google.android.gms/.chimera.PersistentApiService | grep -e "Account Type: .*"`).Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
metadata.NearbyShareChannel = strings.TrimPrefix(strings.TrimSpace(string(nearbyChannel)), "Account Type: ")
return &metadata, nil
}