feat: android input by appium ime

This commit is contained in:
余泓铮
2024-04-08 15:03:40 +08:00
parent df745842d5
commit 58a905328b
10 changed files with 459 additions and 59 deletions
+1 -1
View File
@@ -1 +1 @@
v4.3.7 v4.4.0
+74 -30
View File
@@ -2,8 +2,8 @@ package uixt
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt" "fmt"
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
@@ -22,6 +22,7 @@ import (
) )
const AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME" const AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
const UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
type adbDriver struct { type adbDriver struct {
Driver Driver
@@ -353,14 +354,53 @@ func (ad *adbDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
} }
func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) { func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) {
err = ad.SendUnicodeKeys(text, options...)
if err == nil {
return
}
err = ad.InputText(text, options...)
return
}
func (ad *adbDriver) InputText(text string, options ...ActionOption) (err error) {
// adb shell input text <text> // adb shell input text <text>
_, err = ad.adbClient.RunShellCommand("input", "text", encodeUnicodeText(text)) _, err = ad.adbClient.RunShellCommand("input", "text", text)
if err != nil { if err != nil {
return errors.Wrap(err, "send keys failed") return errors.Wrap(err, "send keys failed")
} }
return nil return nil
} }
func (ad *adbDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
// If the Unicode IME is not installed, fall back to the old interface.
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
// In release version: without the Unicode IME installed, the test cannot execute.
if !ad.IsUnicodeIMEInstalled() {
return fmt.Errorf("appium unicode ime not installed")
}
currentIme, err := ad.GetIme()
if err != nil {
return
}
if currentIme != UnicodeImePackageName {
defer func() {
_ = ad.SetIme(currentIme)
}()
err = ad.SetIme(UnicodeImePackageName)
if err != nil {
log.Warn().Err(err).Msgf("set Unicode Ime failed")
return
}
}
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
if err != nil {
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
return
}
err = ad.InputText("\""+strings.ReplaceAll(encodedStr, "\"", "\\\"")+"\"", options...)
return
}
func (ad *adbDriver) IsAdbKeyBoardInstalled() bool { func (ad *adbDriver) IsAdbKeyBoardInstalled() bool {
output, err := ad.adbClient.RunShellCommand("ime", "list", "-a") output, err := ad.adbClient.RunShellCommand("ime", "list", "-a")
if err != nil { if err != nil {
@@ -369,6 +409,14 @@ func (ad *adbDriver) IsAdbKeyBoardInstalled() bool {
return strings.Contains(output, AdbKeyBoardPackageName) return strings.Contains(output, AdbKeyBoardPackageName)
} }
func (ad *adbDriver) IsUnicodeIMEInstalled() bool {
output, err := ad.adbClient.RunShellCommand("ime", "list", "-s")
if err != nil {
return false
}
return strings.Contains(output, UnicodeImePackageName)
}
func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) { func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) {
defer func() { defer func() {
// Reset to default, don't care which keyboard was chosen before switch: // Reset to default, don't care which keyboard was chosen before switch:
@@ -561,6 +609,30 @@ func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed") return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
} }
func (ad *adbDriver) SetIme(ime string) error {
_, err := ad.adbClient.RunShellCommand("ime", "set", ime)
if err != nil {
return err
}
// even if the shell command has returned,
// as there might be a situation where the input method has not been completely switched yet
// Listen to the following message.
// InputMethodManagerService: onServiceConnected, name:ComponentInfo{io.appium.settings/io.appium.settings.UnicodeIME}, token:android.os.Binder@44f825
// But there is no such log on Vivo.
time.Sleep(3 * time.Second)
return nil
}
func (ad *adbDriver) GetIme() (ime string, err error) {
currentIme, err := ad.adbClient.RunShellCommand("settings", "get", "secure", "default_input_method")
if err != nil {
log.Warn().Err(err).Msgf("get default ime failed")
return
}
currentIme = strings.TrimSpace(currentIme)
return currentIme, nil
}
func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...string) error { func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...string) error {
log.Debug().Str("package_name", packageName). log.Debug().Str("package_name", packageName).
Strs("activity_type", activityType). Strs("activity_type", activityType).
@@ -621,34 +693,6 @@ func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...str
"assert foreground activity failed") "assert foreground activity failed")
} }
func encodeUnicode(c int32) string {
var buffer bytes.Buffer
// Convert each rune (character) into two bytes
buffer.WriteByte(byte(c >> 8))
buffer.WriteByte(byte(c & 0xFF))
// Convert buffer bytes to base64 encoding
encoded := base64.StdEncoding.EncodeToString(buffer.Bytes())
// Replace "/" with "," and remove trailing "="
encoded = strings.ReplaceAll(encoded, "/", ",")
return strings.TrimRight(encoded, "=")
}
func encodeUnicodeText(text string) string {
text = strings.ReplaceAll(text, "&", "&-")
var sb strings.Builder
sb.WriteRune('"')
for _, c := range text {
if c <= 127 {
sb.WriteRune(c)
} else {
// Encode non-ASCII character and append it
sb.WriteString("&" + encodeUnicode(c) + "-")
}
}
sb.WriteRune('"')
return sb.String()
}
var androidActivities = map[string]map[string][]string{ var androidActivities = map[string]map[string][]string{
// DY // DY
"com.ss.android.ugc.aweme": { "com.ss.android.ugc.aweme": {
+16 -3
View File
@@ -3,17 +3,18 @@ package uixt
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"fmt" "fmt"
"github.com/httprunner/funplugin/myexec"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"net" "net"
"os/exec" "os/exec"
"strings" "strings"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb" "github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
) )
@@ -147,6 +148,18 @@ func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
return deviceList, nil return deviceList, nil
} }
func encodeUnicode(c int32) string {
var buffer bytes.Buffer
// Convert each rune (character) into two bytes
buffer.WriteByte(byte(c >> 8))
buffer.WriteByte(byte(c & 0xFF))
// Convert buffer bytes to base64 encoding
encoded := base64.StdEncoding.EncodeToString(buffer.Bytes())
// Replace "/" with "," and remove trailing "="
encoded = strings.ReplaceAll(encoded, "/", ",")
return strings.TrimRight(encoded, "=")
}
type AndroidDevice struct { type AndroidDevice struct {
d *gadb.Device d *gadb.Device
logcat *AdbLogcat logcat *AdbLogcat
@@ -562,7 +575,7 @@ func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
// //
// For example, to simulate a user click on // For example, to simulate a user click on
// the third image that is enabled in a UI screen, you // the third image that is enabled in a UI screen, you
// could specify a a search criteria where the instance is // could specify a search criteria where the instance is
// 2, the `className(String)` matches the image // 2, the `className(String)` matches the image
// widget class, and `enabled(boolean)` is true. // widget class, and `enabled(boolean)` is true.
// The code would look like this: // The code would look like this:
+5 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"testing" "testing"
"time" "time"
) )
@@ -18,7 +19,7 @@ var (
func setupAndroid(t *testing.T) { func setupAndroid(t *testing.T) {
device, err := NewAndroidDevice() device, err := NewAndroidDevice()
checkErr(t, err) checkErr(t, err)
//device.UIA2 = true device.UIA2 = false
driverExt, err = device.NewDriver() driverExt, err = device.NewDriver()
checkErr(t, err) checkErr(t, err)
} }
@@ -244,15 +245,13 @@ func TestDriver_Drag(t *testing.T) {
} }
func TestDriver_SendKeys(t *testing.T) { func TestDriver_SendKeys(t *testing.T) {
driver, err := NewUIADriver(nil, uiaServerURL) setupAndroid(t)
err := driverExt.Driver.SendKeys("Android\"输入速度测试", WithIdentifier("test"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = driver.SendKeys("abc")
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
//err = driver.SendKeys("def") //err = driver.SendKeys("def")
+66 -12
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -452,21 +453,74 @@ func (ud *uiaDriver) SendKeys(text string, options ...ActionOption) (err error)
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
actionOptions := NewActionOptions(options...) actionOptions := NewActionOptions(options...)
data := map[string]interface{}{ err = ud.SendUnicodeKeys(text, options...)
"text": text,
}
// new data options in post data for extra uiautomator configurations
actionOptions.updateData(data)
_, err = ud.httpPOST(data, "/session", ud.sessionId, "keys")
if err != nil { if err != nil {
// use com.android.adbkeyboard if existed data := map[string]interface{}{
if ud.IsAdbKeyBoardInstalled() { "text": text,
err = ud.SendKeysByAdbKeyBoard(text) }
} else {
_, err = ud.adbClient.RunShellCommand("input", "text", text) // new data options in post data for extra uiautomator configurations
actionOptions.updateData(data)
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/keys")
}
return
}
func (ud *uiaDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
// If the Unicode IME is not installed, fall back to the old interface.
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
// In release version: without the Unicode IME installed, the test cannot execute.
if !ud.IsUnicodeIMEInstalled() {
return fmt.Errorf("appium unicode ime not installed")
}
currentIme, err := ud.adbDriver.GetIme()
if err != nil {
return
}
if currentIme != UnicodeImePackageName {
defer func() {
_ = ud.adbDriver.SetIme(currentIme)
}()
err = ud.adbDriver.SetIme(UnicodeImePackageName)
if err != nil {
log.Warn().Err(err).Msgf("set Unicode Ime failed")
return
} }
} }
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
if err != nil {
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
return
}
err = ud.SendActionKey(encodedStr, options...)
return
}
func (ud *uiaDriver) SendActionKey(text string, options ...ActionOption) (err error) {
actionOptions := NewActionOptions(options...)
var actions []interface{}
for i, c := range text {
actions = append(actions, map[string]interface{}{"type": "keyDown", "value": string(c)},
map[string]interface{}{"type": "keyUp", "value": string(c)})
if i != len(text)-1 {
actions = append(actions, map[string]interface{}{"type": "pause", "duration": 40})
}
}
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "key",
"id": "key",
"actions": actions,
},
},
}
// new data options in post data for extra uiautomator configurations
actionOptions.updateData(data)
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions/keys")
return return
} }
+2 -2
View File
@@ -555,8 +555,8 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...Action
// update data options in post data for extra WDA configurations // update data options in post data for extra WDA configurations
actionOptions.updateData(data) actionOptions.updateData(data)
// wda 43 version
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration") _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/drag")
return return
} }
+21 -5
View File
@@ -10,17 +10,23 @@ import (
) )
var ( var (
bundleId = "com.apple.Preferences" bundleId = "com.apple.Preferences"
driver WebDriver driver WebDriver
iOSDriverExt *DriverExt
) )
func setup(t *testing.T) { func setup(t *testing.T) {
device, err := NewIOSDevice() device, err := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
capabilities := NewCapabilities()
driver, err = device.NewUSBDriver(nil) capabilities.WithDefaultAlertAction(AlertActionAccept)
driver, err = device.NewUSBDriver(capabilities)
if err != nil {
t.Fatal(err)
}
iOSDriverExt, err = newDriverExt(device, driver, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -267,6 +273,16 @@ func Test_remoteWD_Drag(t *testing.T) {
} }
} }
func Test_Relative_Drag(t *testing.T) {
setup(t)
// err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5))
err := iOSDriverExt.SwipeRelative(0.5, 0.7, 0.5, 0.5)
if err != nil {
t.Fatal(err)
}
}
func Test_remoteWD_SetPasteboard(t *testing.T) { func Test_remoteWD_SetPasteboard(t *testing.T) {
setup(t) setup(t)
+149
View File
@@ -0,0 +1,149 @@
package utf7
import (
"errors"
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/transform"
)
// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7.
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
type decoder struct {
ascii bool
}
func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
for i := 0; i < len(src); i++ {
ch := src[i]
if ch < min || ch > max { // Illegal code point in ASCII mode
err = ErrInvalidUTF7
return
}
if ch != '&' {
if nDst+1 > len(dst) {
err = transform.ErrShortDst
return
}
nSrc++
dst[nDst] = ch
nDst++
d.ascii = true
continue
}
// Find the end of the Base64 or "&-" segment
start := i + 1
for i++; i < len(src) && src[i] != '-'; i++ {
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
err = ErrInvalidUTF7
return
}
}
if i == len(src) { // Implicit shift ("&...")
if atEOF {
err = ErrInvalidUTF7
} else {
err = transform.ErrShortSrc
}
return
}
var b []byte
if i == start { // Escape sequence "&-"
b = []byte{'&'}
d.ascii = true
} else { // Control or non-ASCII code points in base64
if !d.ascii { // Null shift ("&...-&...-")
err = ErrInvalidUTF7
return
}
b = decode(src[start:i])
d.ascii = false
}
if len(b) == 0 { // Bad encoding
err = ErrInvalidUTF7
return
}
if nDst+len(b) > len(dst) {
d.ascii = true
err = transform.ErrShortDst
return
}
nSrc = i + 1
for _, ch := range b {
dst[nDst] = ch
nDst++
}
}
if atEOF {
d.ascii = true
}
return
}
func (d *decoder) Reset() {
d.ascii = true
}
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
// A nil slice is returned if the encoding is invalid.
func decode(b64 []byte) []byte {
var b []byte
// Allocate a single block of memory large enough to store the Base64 data
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
// double the space allocation for UTF-8.
if n := len(b64); b64[n-1] == '=' {
return nil
} else if n&3 == 0 {
b = make([]byte, b64Enc.DecodedLen(n)*3)
} else {
n += 4 - n&3
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
copy(b[copy(b, b64):n], []byte("=="))
b64, b = b[:n], b[n:]
}
// Decode Base64 into the first 1/3rd of b
n, err := b64Enc.Decode(b, b64)
if err != nil || n&1 == 1 {
return nil
}
// Decode UTF-16-BE into the remaining 2/3rds of b
b, s := b[:n], b[n:]
j := 0
for i := 0; i < n; i += 2 {
r := rune(b[i])<<8 | rune(b[i+1])
if utf16.IsSurrogate(r) {
if i += 2; i == n {
return nil
}
r2 := rune(b[i])<<8 | rune(b[i+1])
if r = utf16.DecodeRune(r, r2); r == repl {
return nil
}
} else if min <= r && r <= max {
return nil
}
j += utf8.EncodeRune(s[j:], r)
}
return s[:j]
}
+91
View File
@@ -0,0 +1,91 @@
package utf7
import (
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/transform"
)
type encoder struct{}
func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
for i := 0; i < len(src); {
ch := src[i]
var b []byte
if min <= ch && ch <= max {
b = []byte{ch}
if ch == '&' {
b = append(b, '-')
}
i++
} else {
start := i
// Find the next printable ASCII code point
i++
for i < len(src) && (src[i] < min || src[i] > max) {
i++
}
if !atEOF && i == len(src) {
err = transform.ErrShortSrc
return
}
b = encode(src[start:i])
}
if nDst+len(b) > len(dst) {
err = transform.ErrShortDst
return
}
nSrc = i
for _, ch := range b {
dst[nDst] = ch
nDst++
}
}
return
}
func (e *encoder) Reset() {}
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
// removes the padding, and adds UTF-7 shifts.
func encode(s []byte) []byte {
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
// control code points (see table below).
b := make([]byte, 0, len(s)+4)
for len(s) > 0 {
r, size := utf8.DecodeRune(s)
if r > utf8.MaxRune {
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
}
s = s[size:]
if r1, r2 := utf16.EncodeRune(r); r1 != repl {
b = append(b, byte(r1>>8), byte(r1))
r = r2
}
b = append(b, byte(r>>8), byte(r))
}
// Encode as base64
n := b64Enc.EncodedLen(len(b)) + 2
b64 := make([]byte, n)
b64Enc.Encode(b64[1:], b)
// Strip padding
n -= 2 - (len(b)+2)%3
b64 = b64[:n]
// Add UTF-7 shifts
b64[0] = '&'
b64[n-1] = '-'
return b64
}
+34
View File
@@ -0,0 +1,34 @@
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
package utf7
import (
"encoding/base64"
"golang.org/x/text/encoding"
)
const (
min = 0x20 // Minimum self-representing UTF-7 value
max = 0x7E // Maximum self-representing UTF-7 value
repl = '\uFFFD' // Unicode replacement code point
)
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
type enc struct{}
func (e enc) NewDecoder() *encoding.Decoder {
return &encoding.Decoder{
Transformer: &decoder{true},
}
}
func (e enc) NewEncoder() *encoding.Encoder {
return &encoding.Encoder{
Transformer: &encoder{},
}
}
// Encoding is the modified UTF-7 encoding.
var Encoding encoding.Encoding = enc{}