Skip to content

Commit

Permalink
Get clipboard/pasteboard (#104)
Browse files Browse the repository at this point in the history
* Add new API endpoint to provider component to get Android and iOS clipboard/pasteboard
* Add in the hub UI a new component for getting and displaying the clipboard/pasteboard value
  • Loading branch information
shamanec authored Jun 4, 2024
1 parent 6265b68 commit 696b076
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import Apps from "./Apps/Apps";
import { Alert, Box, CircularProgress, Stack, TextField } from "@mui/material";
import { Auth } from "../../../../contexts/Auth";
import { api } from '../../../../services/api.js'
import Clipboard from "./Clipboard";

export default function Actions({ deviceData }) {
return (
<Box marginTop='10px'>
<Stack>
<Apps deviceData={deviceData}></Apps>
<TypeText deviceData={deviceData}></TypeText>
<Clipboard deviceData={deviceData}></Clipboard>
</Stack>
</Box>
)
Expand Down
71 changes: 71 additions & 0 deletions hub/gads-ui/src/components/DeviceControl/Tabs/Actions/Clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {Box, Button, CircularProgress, Stack, TextField} from "@mui/material";
import InstallMobileIcon from "@mui/icons-material/InstallMobile";
import {api} from "../../../../services/api";
import {useState} from "react";


export default function Clipboard({ deviceData }) {
const [isGettingCb, setIsGettingCb] = useState(false)
const [cbValue, setCbValue] = useState("")

function handleGetClipboard() {
setIsGettingCb(true)
const url = `/device/${deviceData.udid}/getClipboard`
setCbValue("")
api.get(url)
.then((response) => {
setCbValue(response.data)
setIsGettingCb(false)
})
.catch(() => {
setIsGettingCb(false)
});
}

return (
<Box
marginTop='10px'
style={{
backgroundColor: "#9ba984",
width: "600px"
}}
>
<Stack
style={{
marginLeft: "10px",
marginBottom: "10px",
marginRight: "10px"
}}
>
<h3>Get clipboard value</h3>
{deviceData.os === "ios" &&
<h5 style={{marginTop: "1px"}}>On iOS devices WebDriverAgent has to be the active app to get the pasteboard value, it will be activated and then you'll be navigated to Springboard</h5>
}
<Button
onClick={handleGetClipboard}
id='clipboard-button'
variant='contained'
disabled={isGettingCb}
style={{
backgroundColor: isGettingCb ? "rgba(51,71,110,0.47)" : "#2f3b26",
color: "#9ba984",
fontWeight: "bold"
}}
>Get clipboard</Button>
{isGettingCb &&
<CircularProgress id='progress-indicator' size={30} />
}
<TextField
id="outlined-basic"
variant="outlined"
value={cbValue}
style={{
backgroundColor: '#9ba984',
marginTop: '15px',
width: '100%'
}}
/>
</Stack>
</Box>
)
}
73 changes: 73 additions & 0 deletions provider/router/appium.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var netClient = &http.Client{

func appiumRequest(device *models.Device, method, endpoint string, requestBody io.Reader) (*http.Response, error) {
url := fmt.Sprintf("http://localhost:%s/session/%s/%s", device.AppiumPort, device.AppiumSessionID, endpoint)
device.Logger.LogDebug("appium_interact", fmt.Sprintf("Calling `%s` for device `%s`", url, device.UDID))
req, err := http.NewRequest(method, url, requestBody)
if err != nil {
return nil, err
Expand All @@ -27,6 +28,7 @@ func appiumRequest(device *models.Device, method, endpoint string, requestBody i

func wdaRequest(device *models.Device, method, endpoint string, requestBody io.Reader) (*http.Response, error) {
url := fmt.Sprintf("http://localhost:%v/%s", device.WDAPort, endpoint)
device.Logger.LogDebug("wda_interact", fmt.Sprintf("Calling `%s` for device `%s`", url, device.UDID))
req, err := http.NewRequest(method, url, requestBody)
if err != nil {
return nil, err
Expand All @@ -36,6 +38,7 @@ func wdaRequest(device *models.Device, method, endpoint string, requestBody io.R

func appiumRequestNoSession(device *models.Device, method, endpoint string, requestBody io.Reader) (*http.Response, error) {
url := fmt.Sprintf("http://localhost:%s/%s", device.AppiumPort, endpoint)
device.Logger.LogDebug("appium_interact", fmt.Sprintf("Calling `%s` for device `%s`", url, device.UDID))
req, err := http.NewRequest(method, url, requestBody)
if err != nil {
return nil, err
Expand Down Expand Up @@ -294,3 +297,73 @@ func appiumHome(device *models.Device) (*http.Response, error) {
return nil, fmt.Errorf("Unsupported device OS: %s", device.OS)
}
}

func appiumActivateApp(device *models.Device, appIdentifier string) (*http.Response, error) {
switch device.OS {
case "ios":
requestBody := struct {
BundleId string `json:"bundleId"`
}{
BundleId: appIdentifier,
}

reqJson, err := json.MarshalIndent(requestBody, "", " ")
if err != nil {
return nil, fmt.Errorf("appiumActivateApp: Failed to marshal request body json when activating app for device `%s` - %s", device.UDID, err)
}

return appiumRequest(device, http.MethodPost, "appium/device/activate_app", bytes.NewReader(reqJson))
case "android":
requestBody := struct {
AppId string `json:"appId"`
}{
AppId: appIdentifier,
}

reqJson, err := json.MarshalIndent(requestBody, "", " ")
if err != nil {
return nil, fmt.Errorf("appiumActivateApp: Failed to marshal request body json when activating app for device `%s` - %s", device.UDID, err)
}

return appiumRequest(device, http.MethodPost, "appium/device/activate_app", bytes.NewReader(reqJson))
default:
return nil, fmt.Errorf("appiumActivateApp: Bad device OS for device `%s` - %s", device.UDID, device.OS)
}
}

func appiumGetClipboard(device *models.Device) (*http.Response, error) {
requestBody := struct {
ContentType string `json:"contentType"`
}{
ContentType: "plaintext",
}
reqJson, err := json.MarshalIndent(requestBody, "", " ")
if err != nil {
return nil, fmt.Errorf("appiumGetClipboard: Failed to marshal request body json when getting clipboard for device `%s` - %s", device.UDID, err)
}

switch device.OS {
case "ios":
activateAppResp, err := appiumActivateApp(device, config.Config.EnvConfig.WdaBundleID)
if err != nil {
return activateAppResp, fmt.Errorf("appiumGetClipboard: Failed to activate app - %s", err)
}
defer activateAppResp.Body.Close()

clipboardResp, err := appiumRequest(device, http.MethodPost, "appium/device/get_clipboard", bytes.NewReader(reqJson))
if err != nil {
return clipboardResp, fmt.Errorf("appiumGetClipboard: Failed to execute Appium request for device `%s` - %s", device.UDID, err)
}

_, err = appiumHome(device)
if err != nil {
device.Logger.LogWarn("appium_interact", "appiumGetClipboard: Failed to navigate to Home/Springboard using Appium")
}

return clipboardResp, nil
case "android":
return appiumRequest(device, http.MethodPost, "appium/device/get_clipboard", bytes.NewReader(reqJson))
default:
return nil, fmt.Errorf("appiumGetClipboard: Bad device OS for device `%s` - %s", device.UDID, device.OS)
}
}
38 changes: 38 additions & 0 deletions provider/router/device_routes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package router

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -69,6 +70,43 @@ func DeviceHome(c *gin.Context) {
fmt.Fprintf(c.Writer, string(homeResponseBody))
}

func DeviceGetClipboard(c *gin.Context) {
udid := c.Param("udid")
device := devices.DeviceMap[udid]
device.Logger.LogInfo("appium_interact", "Getting device clipboard value")

// Send the request
clipboardResponse, err := appiumGetClipboard(device)
if err != nil {
device.Logger.LogError("appium_interact", fmt.Sprintf("Failed to get device clipboard value - %s", err))
c.String(http.StatusInternalServerError, "")
return
}
defer clipboardResponse.Body.Close()

// Read the response body
clipboardResponseBody, err := io.ReadAll(clipboardResponse.Body)
if err != nil {
device.Logger.LogError("appium_interact", fmt.Sprintf("Failed to read clipboard response body while getting clipboard value - %s", err))
c.String(http.StatusInternalServerError, "")
return
}

// Unmarshal the response body to get the actual value returned
valueResp := struct {
Value string `json:"value"`
}{}
err = json.Unmarshal(clipboardResponseBody, &valueResp)
if err != nil {
device.Logger.LogError("appium_interact", fmt.Sprintf("Failed to unmarshal clipboard response body - %s", err))
c.String(http.StatusInternalServerError, "")
}

// Decode the value because Appium returns it as base64 encoded string
decoded, _ := base64.StdEncoding.DecodeString(valueResp.Value)
c.String(http.StatusOK, string(decoded))
}

// Call respective Appium/WDA endpoint to lock the device
func DeviceLock(c *gin.Context) {
udid := c.Param("udid")
Expand Down
1 change: 1 addition & 0 deletions provider/router/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func HandleRequests() *gin.Engine {
deviceGroup.GET("/appiumSource", DeviceAppiumSource)
deviceGroup.POST("/typeText", DeviceTypeText)
deviceGroup.POST("/clearText", DeviceClearText)
deviceGroup.GET("/getClipboard", DeviceGetClipboard)
deviceGroup.Any("/appium/*proxyPath", AppiumReverseProxy)
deviceGroup.GET("/android-stream", AndroidStreamProxy)
deviceGroup.GET("/android-stream-mjpeg", AndroidStreamMJPEG)
Expand Down

0 comments on commit 696b076

Please sign in to comment.