├── .editorconfig ├── .gitignore ├── .gometalinter.json ├── LICENSE ├── README.md ├── app.go ├── device.go ├── errs.go ├── events.go ├── examples ├── app.go └── selector.go ├── gesture.go ├── input.go ├── screen.go ├── selector.go ├── shell.go ├── toast.go ├── uiautomator.go └── watcher.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 4 12 | charset = utf-8 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Other 6 | # 7 | dist/ 8 | release/ 9 | *-lock.json 10 | 11 | # Xcode 12 | # 13 | build/ 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata 23 | *.xccheckout 24 | *.moved-aside 25 | DerivedData 26 | *.hmap 27 | *.ipa 28 | *.xcuserstate 29 | project.xcworkspace 30 | 31 | # Android/IntelliJ 32 | # 33 | build/ 34 | .idea 35 | .gradle 36 | local.properties 37 | *.iml 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | android/app/libs 43 | *.keystore 44 | 45 | # Golang 46 | vendor/ 47 | go.mod 48 | go.sum 49 | -------------------------------------------------------------------------------- /.gometalinter.json: -------------------------------------------------------------------------------- 1 | { 2 | "Enable": ["deadcode", "unconvert"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 var.darling@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This is a simple golang wrapper for working uiautomator. 3 | 4 | ## Setup 5 | 6 | 7 | To install this library, simple: 8 | 9 | 10 | 11 | ```bash 12 | go get -u github.com/trazyn/uiautomator-go 13 | ``` 14 | 15 | 16 | Import the package: 17 | 18 | 19 | 20 | ```go 21 | import ug "github.com/trazyn/uiautomator-go" 22 | ``` 23 | 24 | 25 | ## Quick start 26 | 27 | First, let yours mobile and PC join the same network. 28 | 29 | ```go 30 | ua := ug.New(&ug.Config{ 31 | Host: "10.10.20.78", 32 | Port: 7912, 33 | }) 34 | 35 | ua.Unlock() 36 | 37 | // Show toast 38 | toast := ua.NewToast() 39 | toast.Show("hallo world", 10) 40 | ``` 41 | 42 | [https://github.com/openatx/uiautomator2#basic-api-usages](https://github.com/openatx/uiautomator2#basic-api-usages) 43 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | /** 2 | https://github.com/openatx/uiautomator2#app-management 3 | */ 4 | package uiautomator 5 | 6 | /* 7 | Install an app 8 | TODO: api "/install" not work 9 | */ 10 | func (ua *UIAutomator) AppInstall(url string) error { 11 | return nil 12 | } 13 | 14 | /* 15 | Launch app 16 | */ 17 | func (ua *UIAutomator) AppStart(packageName string) error { 18 | _, err := ua.Shell( 19 | []string{ 20 | "monkey", "-p", packageName, "-c", 21 | "android.intent.category.LAUNCHER", "1", 22 | }, 23 | 10, 24 | ) 25 | 26 | return err 27 | } 28 | 29 | /* 30 | Stop app 31 | */ 32 | func (ua *UIAutomator) AppStop(packageName string) error { 33 | _, err := ua.Shell( 34 | []string{ 35 | "am", "force-stop", packageName, 36 | }, 37 | 10, 38 | ) 39 | 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | /** 2 | Device api related 3 | https://github.com/openatx/uiautomator2#retrieve-the-device-info 4 | */ 5 | package uiautomator 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | type ( 14 | DeviceInfo struct { 15 | CurrentPackageName string `json:"currentPackageName"` 16 | DisplayHeight int `json:"displayHeight"` 17 | DisplayWidth int `json:"displayWidth"` 18 | DisplayRotation int `json:"displayRotation"` 19 | DisplaySizeDpX int `json:"displaySizeDpX"` 20 | DisplaySizeDpY int `json:"displaySizeDpY"` 21 | ProductName string `json:"productName"` 22 | ScreenOn bool `json:"screenOn"` 23 | SdkInt int `json:"sdkInt"` 24 | NaturalOrientation bool `json:"naturalOrientation"` 25 | } 26 | 27 | WindowSize struct { 28 | Width int `json:"width"` 29 | Height int `json:"height"` 30 | } 31 | 32 | AppInfo struct { 33 | Activity string `json:"activity"` 34 | Package string `json:"package"` 35 | } 36 | ) 37 | 38 | /* 39 | Get basic information 40 | */ 41 | func (ua *UIAutomator) GetDeviceInfo() (*DeviceInfo, error) { 42 | result := &DeviceInfo{} 43 | 44 | return result, ua.post( 45 | &RPCOptions{ 46 | Method: "deviceInfo", 47 | Params: []interface{}{}, 48 | }, 49 | result, 50 | nil, 51 | ) 52 | } 53 | 54 | /* 55 | Get window size 56 | */ 57 | func (ua *UIAutomator) GetWindowSize() (*WindowSize, error) { 58 | var RPCReturned struct { 59 | Display *WindowSize `json:"display"` 60 | } 61 | transform := func(response *http.Response) error { 62 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 63 | if err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | return RPCReturned.Display, ua.get( 70 | &RPCOptions{ 71 | URL: "info", 72 | }, 73 | nil, 74 | transform, 75 | ) 76 | } 77 | 78 | /* 79 | Get current app info 80 | */ 81 | func (ua *UIAutomator) GetCurrentApp() (info *AppInfo, err error) { 82 | output, err := ua.Shell([]string{"dumpsys", "window", "windows"}, 10) 83 | if err != nil { 84 | return 85 | } 86 | 87 | r := regexp.MustCompile(`mCurrentFocus=Window{.*\s+(?P[^\s]+)/(?P[^\s]+)\}`) 88 | matched := r.FindStringSubmatch(output) 89 | res := make(map[string]string) 90 | 91 | for i, name := range r.SubexpNames() { 92 | if i != 0 && len(name) > 0 { 93 | res[name] = matched[i] 94 | } 95 | } 96 | 97 | info = &AppInfo{ 98 | Package: res["package"], 99 | Activity: res["activity"], 100 | } 101 | return 102 | } 103 | 104 | /* 105 | Get device serial number 106 | */ 107 | func (ua *UIAutomator) GetSerialNumber() (string, error) { 108 | var RPCReturned struct { 109 | Serial string `json:"serial"` 110 | } 111 | transform := func(response *http.Response) error { 112 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 113 | if err != nil { 114 | return err 115 | } 116 | return nil 117 | } 118 | 119 | return RPCReturned.Serial, ua.get( 120 | &RPCOptions{ 121 | URL: "info", 122 | }, 123 | nil, 124 | transform, 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /errs.go: -------------------------------------------------------------------------------- 1 | package uiautomator 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | type ( 10 | GatewayError struct { 11 | Message string 12 | } 13 | SessionError struct { 14 | Message string 15 | } 16 | UiaError struct { 17 | Code int `json:"code"` 18 | Message string `json:"message"` 19 | } 20 | ) 21 | 22 | func (err *GatewayError) Error() string { 23 | return err.Message 24 | } 25 | 26 | func (err *SessionError) Error() string { 27 | return err.Message 28 | } 29 | 30 | func (err *UiaError) Error() string { 31 | return err.Message 32 | } 33 | 34 | func boom(response *http.Response) error { 35 | responseBody, err := ioutil.ReadAll(response.Body) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if response.StatusCode == http.StatusBadGateway { 41 | return &GatewayError{"Gateway error"} 42 | } 43 | 44 | if response.StatusCode == http.StatusGone { 45 | return &SessionError{"App quit or crash"} 46 | } 47 | 48 | return fmt.Errorf("HTTP Return code is not 200: (%d) [%s]", response.StatusCode, responseBody) 49 | } 50 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | /** 2 | Key events api related 3 | https://github.com/openatx/uiautomator2#retrieve-the-device-info 4 | */ 5 | 6 | package uiautomator 7 | 8 | import ( 9 | "reflect" 10 | "time" 11 | ) 12 | 13 | /* 14 | Trun on the screen 15 | */ 16 | func (ua *UIAutomator) WakeUp() error { 17 | return ua.post( 18 | &RPCOptions{ 19 | Method: "wakeUp", 20 | Params: []interface{}{}, 21 | }, 22 | nil, 23 | nil, 24 | ) 25 | } 26 | 27 | /* 28 | Trun off the screen 29 | */ 30 | func (ua *UIAutomator) Sleep() error { 31 | return ua.post( 32 | &RPCOptions{ 33 | Method: "sleep", 34 | Params: []interface{}{}, 35 | }, 36 | nil, 37 | nil, 38 | ) 39 | } 40 | 41 | /* 42 | Check current screen status 43 | */ 44 | func (ua *UIAutomator) checkScreenStatus(wakeUpOeSleep bool) (bool, error) { 45 | info, err := ua.GetDeviceInfo() 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | return info.ScreenOn == wakeUpOeSleep, nil 51 | } 52 | 53 | /* 54 | Check device is wakeup 55 | */ 56 | func (ua *UIAutomator) IsWakeUp() (res bool, err error) { 57 | res, err = ua.checkScreenStatus(true) 58 | return 59 | } 60 | 61 | /* 62 | Check device is sleep 63 | */ 64 | func (ua *UIAutomator) IsSleep() (res bool, err error) { 65 | res, err = ua.checkScreenStatus(false) 66 | return 67 | } 68 | 69 | /* 70 | Press key 71 | */ 72 | func (ua *UIAutomator) Press(key string) error { 73 | return ua.post( 74 | &RPCOptions{ 75 | Method: "pressKey", 76 | Params: []interface{}{key}, 77 | }, 78 | nil, 79 | nil, 80 | ) 81 | } 82 | 83 | /* 84 | Press key code 85 | */ 86 | func (ua *UIAutomator) PressKeyCode(key int, meta interface{}) error { 87 | params := []interface{}{key} 88 | 89 | if reflect.TypeOf(meta).Kind() == reflect.Int { 90 | params = append(params, meta) 91 | } 92 | 93 | return ua.post( 94 | &RPCOptions{ 95 | Method: "pressKeyCode", 96 | Params: params, 97 | }, 98 | nil, 99 | nil, 100 | ) 101 | } 102 | 103 | /* 104 | Unblock the device 105 | */ 106 | func (ua *UIAutomator) Unlock() error { 107 | var done = make(chan bool) 108 | 109 | // This call will cause blocking, after 1s press home 110 | go func() { 111 | time.AfterFunc( 112 | time.Duration(1000)*time.Millisecond, 113 | func() { 114 | done <- true 115 | }, 116 | ) 117 | ua.Shell( 118 | []string{"am start -W -n com.github.uiautomator/.IdentifyActivity -e theme black"}, 119 | 0, 120 | ) 121 | }() 122 | 123 | <-done 124 | 125 | return ua.Press("home") 126 | } 127 | -------------------------------------------------------------------------------- /examples/app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | ug "uiautomator" 5 | ) 6 | 7 | func main() { 8 | ua := ug.New(&ug.Config{ 9 | Host: "10.10.60.126", 10 | Port: 7912, 11 | AutoRetry: 0, 12 | Timeout: 10, 13 | }) 14 | 15 | ua.Watchman(). 16 | Remove("CIB_RESOLVE_TIMEOUT"). 17 | Register( 18 | "CIB_RESOLVE_TIMEOUT", 19 | map[string]interface{}{ 20 | "text": "操作超时,请重新登录", 21 | }, 22 | ). 23 | Click( 24 | map[string]interface{}{ 25 | "text": "重新启动", 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /examples/selector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ug "uiautomator" 6 | ) 7 | 8 | func main() { 9 | ua := ug.New(&ug.Config{ 10 | Host: "10.10.60.19", 11 | Port: 7912, 12 | AutoRetry: 0, 13 | Timeout: 10, 14 | }) 15 | 16 | eles := make([]*ug.Element, 0) 17 | ele := ua.GetElementBySelector(map[string]interface{}{"className": "android.widget.ScrollView"}) 18 | fmt.Println(ele.Count()) 19 | 20 | ele = ele.Child(map[string]interface{}{"className": "android.view.ViewGroup"}) 21 | count, _ := ele.Count() 22 | fmt.Println("count:", count) 23 | 24 | height := 139 25 | for i := 0; i < count; i++ { 26 | eleItem := ele.Eq(i) 27 | rect, _ := eleItem.GetRect() 28 | fmt.Println(rect) 29 | if rect.Bottom-rect.Top == height { 30 | eles = append(eles, eleItem) 31 | fmt.Println(eleItem.GetRect()) 32 | // str := parseElement(eleItem) 33 | // fmt.Println(str) 34 | i += 4 35 | } 36 | } 37 | fmt.Println("eles:", len(eles)) 38 | fmt.Println(eles) 39 | 40 | fmt.Println("获取到的元素!!!!!") 41 | for _, e := range eles { 42 | rect, _ := e.GetRect() 43 | fmt.Println("rect:", rect) 44 | } 45 | 46 | /* 47 | // Get child element 48 | 49 | ele, err = ele.ChildByText( 50 | "Clock", 51 | map[string]interface{}{ 52 | "className": "android.widget.FrameLayout", 53 | }, 54 | ) 55 | */ 56 | 57 | /* 58 | // Get element by index 59 | 60 | ele, err = ele.Eq(0) 61 | if err != nil { 62 | panic(err) 63 | } 64 | */ 65 | 66 | /* 67 | // Get text 68 | 69 | text, err := ele.GetText() 70 | if err != nil { 71 | panic(err) 72 | } 73 | fmt.Println(text) 74 | */ 75 | 76 | /* 77 | // Set text 78 | 79 | err = ele.SetText("https://www.google.com/") 80 | if err != nil { 81 | panic(err) 82 | } 83 | */ 84 | 85 | /* 86 | // Long click 87 | 88 | err = ele.LongClick() 89 | if err != nil { 90 | panic(err) 91 | } 92 | */ 93 | 94 | /* 95 | // Swipe element 96 | 97 | err = ele.SwipeLeft() 98 | if err != nil { 99 | panic(err) 100 | } 101 | */ 102 | } 103 | -------------------------------------------------------------------------------- /gesture.go: -------------------------------------------------------------------------------- 1 | /** 2 | Gesture interaction with the device 3 | https://github.com/openatx/uiautomator2#gesture-interaction-with-the-device 4 | */ 5 | package uiautomator 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | ) 11 | 12 | type Position struct { 13 | X float32 14 | Y float32 15 | } 16 | 17 | func (pos *Position) String() string { 18 | return fmt.Sprintf("%s, %s", pos.X, pos.Y) 19 | } 20 | 21 | /* 22 | Convert related position to absolute position 23 | */ 24 | func (ua *UIAutomator) rel2abs(rel *Position) *Position { 25 | if rel == nil { 26 | rel = &Position{} 27 | } 28 | 29 | abs := &Position{ 30 | X: rel.X, 31 | Y: rel.Y, 32 | } 33 | size := &WindowSize{} 34 | 35 | if rel.X < 1 || rel.Y < 1 { 36 | if ua.size == nil { 37 | size, _ = ua.GetWindowSize() 38 | 39 | // Cache the window size 40 | ua.size = size 41 | } 42 | 43 | size = ua.size 44 | } 45 | 46 | if rel.X < 1 { 47 | abs.X = float32(size.Width) * rel.X 48 | } 49 | 50 | if rel.Y < 1 { 51 | abs.Y = float32(size.Height) * abs.Y 52 | } 53 | 54 | return abs 55 | } 56 | 57 | /* 58 | Click on the screen 59 | */ 60 | func (ua *UIAutomator) Click(position *Position) error { 61 | if position.X < 0 || position.Y < 0 { 62 | return fmt.Errorf("Click: an invalid position %q", position) 63 | } 64 | 65 | abs := ua.rel2abs(position) 66 | 67 | return ua.post( 68 | &RPCOptions{ 69 | Method: "click", 70 | Params: []interface{}{abs.X, abs.Y}, 71 | }, 72 | nil, 73 | nil, 74 | ) 75 | } 76 | 77 | /* 78 | Double click on the screen 79 | */ 80 | func (ua *UIAutomator) DbClick(position *Position, duration float32) error { 81 | if position.X < 0 || position.Y < 0 { 82 | return fmt.Errorf("DbClick: an invalid position %q", position) 83 | } 84 | 85 | abs := ua.rel2abs(position) 86 | 87 | // First click 88 | if err := ua.Click(abs); err != nil { 89 | return err 90 | } 91 | 92 | time.Sleep(time.Duration(duration*1000) * time.Millisecond) 93 | 94 | // Second click 95 | if err := ua.Click(abs); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (ua *UIAutomator) touch(action int, position *Position) error { 103 | return ua.post( 104 | &RPCOptions{ 105 | Method: "injectInputEvent", 106 | Params: []interface{}{action, position.X, position.Y, 0}, 107 | }, 108 | nil, 109 | nil, 110 | ) 111 | } 112 | 113 | func (ua *UIAutomator) touchDown(position *Position) error { 114 | return ua.touch(0, position) 115 | } 116 | 117 | func (ua *UIAutomator) touchUp(position *Position) error { 118 | return ua.touch(1, position) 119 | } 120 | 121 | func (ua *UIAutomator) touchMove(position *Position) error { 122 | return ua.touch(2, position) 123 | } 124 | 125 | /* 126 | Long click on the screen 127 | */ 128 | func (ua *UIAutomator) LongClick(position *Position, duration float32) error { 129 | if position.X < 0 || position.Y < 0 { 130 | return fmt.Errorf("LongClick: an invalid position %q", position) 131 | } 132 | 133 | abs := ua.rel2abs(position) 134 | 135 | // Default duration is 0.5s 136 | if duration == 0 { 137 | duration = 0.5 138 | } 139 | 140 | if err := ua.touchDown(abs); err != nil { 141 | return err 142 | } 143 | 144 | time.Sleep(time.Duration(duration*1000) * time.Millisecond) 145 | 146 | if err := ua.touchUp(abs); err != nil { 147 | return err 148 | } 149 | return nil 150 | } 151 | 152 | /* 153 | Swipe the screen 154 | */ 155 | func (ua *UIAutomator) Swipe(from *Position, to *Position, step int) error { 156 | if from.X < 0 || from.Y < 0 || to.X < 0 || to.Y < 0 { 157 | return fmt.Errorf("Swipe: invalid from(%s) -> to(%s)", from, to) 158 | } 159 | 160 | from = ua.rel2abs(from) 161 | to = ua.rel2abs(to) 162 | 163 | return ua.post( 164 | &RPCOptions{ 165 | Method: "swipe", 166 | Params: []interface{}{from.X, from.Y, to.X, to.Y, step}, 167 | }, 168 | nil, 169 | nil, 170 | ) 171 | } 172 | 173 | /* 174 | Swipe by points, unlock the gesture login 175 | */ 176 | func (ua *UIAutomator) SwipePoints(points ...*Position) error { 177 | var positions []int 178 | 179 | for _, v := range points { 180 | abs := ua.rel2abs(v) 181 | positions = append(positions, int(abs.X), int(abs.Y)) 182 | } 183 | 184 | return ua.post( 185 | &RPCOptions{ 186 | Method: "swipePoints", 187 | Params: []interface{}{positions, 20}, 188 | }, 189 | nil, 190 | nil, 191 | ) 192 | } 193 | 194 | /* 195 | Swipe the screen 196 | */ 197 | func (ua *UIAutomator) Drag(start *Position, end *Position, duration float32) error { 198 | if start.X < 0 || start.Y < 0 || end.X < 0 || end.Y < 0 { 199 | return fmt.Errorf("Drag: invalid start(%s) -> end(%s)", start, end) 200 | } 201 | 202 | start = ua.rel2abs(start) 203 | end = ua.rel2abs(end) 204 | 205 | return ua.post( 206 | &RPCOptions{ 207 | Method: "drag", 208 | Params: []interface{}{start.X, start.Y, end.X, end.Y, duration * 200}, 209 | }, 210 | nil, 211 | nil, 212 | ) 213 | } 214 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | /** 2 | https://github.com/openatx/uiautomator2#input-method 3 | */ 4 | package uiautomator 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const _FASTIME = "com.github.uiautomator/.FastInputIME" 14 | 15 | var _CODE = map[string]int{ 16 | "go": 2, 17 | "search": 3, 18 | "send": 4, 19 | "next": 5, 20 | "done": 6, 21 | "previous": 7, 22 | } 23 | 24 | /* 25 | Wait FastInputIME is ready 26 | */ 27 | func (ua *UIAutomator) waitFastinputIME() error { 28 | r := regexp.MustCompile(`mCurMethodId=([-_./\w]+)`) 29 | retry := 0 30 | 31 | for { 32 | if retry > 2 { 33 | return fmt.Errorf("FastInputIME started failed") 34 | } 35 | 36 | output, err := ua.Shell([]string{"dumpsys", "input_method"}, 10) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | matchd := r.FindStringSubmatch(output) 42 | 43 | if len(matchd) == 0 || matchd[1] != _FASTIME { 44 | err := ua.SetFastinputIME(true) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // Sleep 0.5s 50 | time.Sleep(time.Duration(500) * time.Millisecond) 51 | retry++ 52 | continue 53 | } 54 | 55 | break 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (ua *UIAutomator) SetFastinputIME(enable bool) error { 62 | if enable { 63 | if _, err := ua.Shell([]string{"ime", "enable", _FASTIME}, 5); err != nil { 64 | return err 65 | } 66 | if _, err := ua.Shell([]string{"ime", "set", _FASTIME}, 5); err != nil { 67 | return err 68 | } 69 | } else { 70 | if _, err := ua.Shell([]string{"ime", "disable", _FASTIME}, 5); err != nil { 71 | return err 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func (ua *UIAutomator) SendAction(code interface{}) error { 78 | if err := ua.waitFastinputIME(); err != nil { 79 | return err 80 | } 81 | 82 | switch typed := code.(type) { 83 | case string: 84 | value, ok := _CODE[typed] 85 | if !ok { 86 | return fmt.Errorf("Unknow code: %q", code) 87 | } 88 | code = value 89 | case int: 90 | // Pass 91 | default: 92 | return fmt.Errorf("Unknow code: %q", code) 93 | } 94 | 95 | if _, err := ua.Shell([]string{"am", "broadcast", "-a", "ADB_EDITOR_CODE", "--ei", "code", strconv.Itoa(code.(int))}, 5); err != nil { 96 | return err 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /screen.go: -------------------------------------------------------------------------------- 1 | /** 2 | Screen api related 3 | https://github.com/openatx/uiautomator2#screen-related 4 | */ 5 | package uiautomator 6 | 7 | import ( 8 | "encoding/base64" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | ORIENTATION_RIGHT = "right" 14 | ORIENTATION_LEFT = "left" 15 | ORIENTATION_NATURAL = "natural" 16 | ORIENTATION_UPSIDEDOWN = "upsidedown" 17 | ) 18 | 19 | type ( 20 | ORIENTATION string 21 | 22 | Screenshot struct { 23 | Type string 24 | Base64 string 25 | } 26 | ) 27 | 28 | func (ua *UIAutomator) setOrientation(orientation ORIENTATION) error { 29 | return ua.post( 30 | &RPCOptions{ 31 | Method: "setOrientation", 32 | Params: []interface{}{}, 33 | }, 34 | nil, 35 | nil, 36 | ) 37 | } 38 | 39 | /* 40 | Set orientation natural 41 | */ 42 | func (ua *UIAutomator) SetOrientationNatural() error { 43 | return ua.setOrientation(ORIENTATION_NATURAL) 44 | } 45 | 46 | /* 47 | Set orientation upsidedown(not worked) 48 | */ 49 | func (ua *UIAutomator) SetOrientationUpsidedown() error { 50 | return ua.setOrientation(ORIENTATION_UPSIDEDOWN) 51 | } 52 | 53 | /* 54 | Set orientation left 55 | */ 56 | func (ua *UIAutomator) SetOrientationLeft() error { 57 | return ua.setOrientation(ORIENTATION_LEFT) 58 | } 59 | 60 | /* 61 | Set orientation right 62 | */ 63 | func (ua *UIAutomator) SetOrientationRight() error { 64 | return ua.setOrientation(ORIENTATION_RIGHT) 65 | } 66 | 67 | /* 68 | Freeze rotation 69 | */ 70 | func (ua *UIAutomator) FreezeRotation(freeze bool) error { 71 | return ua.post( 72 | &RPCOptions{ 73 | Method: "freezeRotation", 74 | Params: []interface{}{}, 75 | }, 76 | nil, 77 | nil, 78 | ) 79 | } 80 | 81 | /** 82 | Open notification 83 | */ 84 | func (ua *UIAutomator) OpenNotification() error { 85 | return ua.post( 86 | &RPCOptions{ 87 | Method: "openNotification", 88 | Params: []interface{}{}, 89 | }, 90 | nil, 91 | nil, 92 | ) 93 | } 94 | 95 | /** 96 | Open quick settings 97 | */ 98 | func (ua *UIAutomator) OpenQuickSettings() error { 99 | return ua.post( 100 | &RPCOptions{ 101 | Method: "openQuickSettings", 102 | Params: []interface{}{}, 103 | }, 104 | nil, 105 | nil, 106 | ) 107 | } 108 | 109 | /** 110 | Get the UI hierarchy dump content (unicoded). 111 | */ 112 | func (ua *UIAutomator) DumpWindowHierarchy() (string, error) { 113 | var xml string 114 | transform := func(payload interface{}, response *http.Response) error { 115 | xml = payload.(string) 116 | return nil 117 | } 118 | 119 | return xml, ua.post( 120 | &RPCOptions{ 121 | Method: "dumpWindowHierarchy", 122 | Params: []interface{}{true}, 123 | }, 124 | nil, 125 | transform, 126 | ) 127 | } 128 | 129 | func (ua *UIAutomator) GetScreenshot() (*Screenshot, error) { 130 | result := &Screenshot{} 131 | transform := func(data interface{}, response *http.Response) error { 132 | // Convert to base64 133 | result.Base64 = base64.StdEncoding.EncodeToString(data.([]byte)) 134 | return nil 135 | } 136 | 137 | return result, ua.get( 138 | &RPCOptions{ 139 | URL: "screenshot/0", 140 | }, 141 | nil, 142 | transform, 143 | ) 144 | } 145 | -------------------------------------------------------------------------------- /selector.go: -------------------------------------------------------------------------------- 1 | /** 2 | Selector is a handy mechanism to identify a specific UI object in the current window. 3 | https://github.com/openatx/uiautomator2#selector 4 | */ 5 | package uiautomator 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type ( 14 | Selector map[string]interface{} 15 | 16 | Element struct { 17 | ua *UIAutomator 18 | position *Position 19 | selector Selector 20 | } 21 | 22 | ElementRect struct { 23 | Bottom int `json:"bottom"` 24 | Left int `json:"left"` 25 | Right int `json:"right"` 26 | Top int `json:"top"` 27 | } 28 | 29 | ElementInfo struct { 30 | ContentDescription string `json:"contentDescription"` 31 | Checked bool `json:"checked"` 32 | Scrollable bool `json:"scrollable"` 33 | Text string `json:"text"` 34 | PackageName string `json:"packageName"` 35 | Selected bool `json:"selected"` 36 | Enabled bool `json:"enabled"` 37 | ClassName string `json:"className"` 38 | Focused bool `json:"focused"` 39 | Focusable bool `json:"focusable"` 40 | Clickable bool `json:"clickable"` 41 | ChileCount int `json:"chileCount"` 42 | LongClickable bool `json:"longClickable"` 43 | Checkable bool `json:"checkable"` 44 | Bounds *ElementRect `json:"bounds"` 45 | VisibleBounds *ElementRect `json:"visibleBounds"` 46 | } 47 | ) 48 | 49 | var _MASK = map[string]int{ 50 | "text": 0x01, // MASK_TEXT, 51 | "textContains": 0x02, // MASK_TEXTCONTAINS, 52 | "textMatches": 0x04, // MASK_TEXTMATCHES, 53 | "textStartsWith": 0x08, // MASK_TEXTSTARTSWITH, 54 | "className": 0x10, // MASK_CLASSNAME 55 | "classNameMatches": 0x20, // MASK_CLASSNAMEMATCHES 56 | "description": 0x40, // MASK_DESCRIPTION 57 | "descriptionContains": 0x80, // MASK_DESCRIPTIONCONTAINS 58 | "descriptionMatches": 0x0100, // MASK_DESCRIPTIONMATCHES 59 | "descriptionStartsWith": 0x0200, // MASK_DESCRIPTIONSTARTSWITH 60 | "checkable": 0x0400, // MASK_CHECKABLE 61 | "checked": 0x0800, // MASK_CHECKED 62 | "clickable": 0x1000, // MASK_CLICKABLE 63 | "longClickable": 0x2000, // MASK_LONGCLICKABLE, 64 | "scrollable": 0x4000, // MASK_SCROLLABLE, 65 | "enabled": 0x8000, // MASK_ENABLED, 66 | "focusable": 0x010000, // MASK_FOCUSABLE, 67 | "focused": 0x020000, // MASK_FOCUSED, 68 | "selected": 0x040000, // MASK_SELECTED, 69 | "packageName": 0x080000, // MASK_PACKAGENAME, 70 | "packageNameMatches": 0x100000, // MASK_PACKAGENAMEMATCHES, 71 | "resourceId": 0x200000, // MASK_RESOURCEID, 72 | "resourceIdMatches": 0x400000, // MASK_RESOURCEIDMATCHES, 73 | "index": 0x800000, // MASK_INDEX, 74 | "instance": 0x01000000, // MASK_INSTANCE, 75 | } 76 | 77 | /* 78 | Get element info 79 | */ 80 | func (ele Element) GetInfo() (*ElementInfo, error) { 81 | var RPCReturned ElementInfo 82 | 83 | if err := ele.ua.post( 84 | &RPCOptions{ 85 | Method: "objInfo", 86 | Params: []interface{}{getParams(ele.selector)}, 87 | }, 88 | &RPCReturned, 89 | nil, 90 | ); err != nil { 91 | return nil, err 92 | } 93 | 94 | return &RPCReturned, nil 95 | } 96 | 97 | /* 98 | Get Widget rect bounds 99 | */ 100 | func (ele Element) GetRect() (rect *ElementRect, err error) { 101 | info, err := ele.GetInfo() 102 | if err != nil { 103 | return 104 | } 105 | 106 | rect = info.Bounds 107 | if rect == nil { 108 | rect = info.VisibleBounds 109 | } 110 | return 111 | } 112 | 113 | /* 114 | Get Widget center point 115 | */ 116 | func (ele Element) Center(offset *Position) (*Position, error) { 117 | rect, err := ele.GetRect() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | lx, ly, rx, ry := rect.Left, rect.Top, rect.Right, rect.Bottom 123 | width, height := rx-lx, ry-ly 124 | 125 | if offset == nil { 126 | offset = &Position{0.5, 0.5} 127 | } 128 | 129 | abs := &Position{} 130 | abs.X = float32(lx) + float32(width)*offset.X 131 | abs.Y = float32(ly) + float32(height)*offset.Y 132 | return abs, nil 133 | } 134 | 135 | /* 136 | Get the count 137 | */ 138 | func (ele Element) Count() (int, error) { 139 | var RPCReturned struct { 140 | Result int `json:"result"` 141 | } 142 | transform := func(response *http.Response) error { 143 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 144 | if err != nil { 145 | return err 146 | } 147 | return nil 148 | } 149 | 150 | return RPCReturned.Result, ele.ua.post( 151 | &RPCOptions{ 152 | Method: "count", 153 | Params: []interface{}{getParams(ele.selector)}, 154 | }, 155 | nil, 156 | transform, 157 | ) 158 | } 159 | 160 | /* 161 | Clone the element 162 | */ 163 | func (ele Element) Clone() *Element { 164 | copied := ele 165 | 166 | copied.selector = make(Selector) 167 | 168 | for k, v := range ele.selector { 169 | copied.selector[k] = v 170 | 171 | if k == "childOrSiblingSelector" { 172 | nested := make([]interface{}, 0) 173 | for _, selector := range v.([]interface{}) { 174 | nested = append(nested, parseSelector(selector.(Selector))) 175 | } 176 | copied.selector[k] = nested 177 | } 178 | } 179 | 180 | return &copied 181 | } 182 | 183 | /* 184 | Get the instance via index 185 | */ 186 | func (ele Element) Eq(index int) *Element { 187 | copied := ele.Clone() 188 | 189 | // Check is a child selector 190 | childOrSiblingSelector := copied.selector["childOrSiblingSelector"].([]interface{}) 191 | lastSelectorIndex := len(childOrSiblingSelector) 192 | 193 | if lastSelectorIndex > 0 { 194 | // Get the child selector 195 | lastSelector := childOrSiblingSelector[lastSelectorIndex-1].(Selector) 196 | 197 | // Update the child selector 198 | lastSelector["instance"] = index 199 | newSelector := parseSelector(lastSelector) 200 | childOrSiblingSelector[lastSelectorIndex-1] = newSelector 201 | } else { 202 | // Update the selector 203 | copied.selector["instance"] = index 204 | 205 | // Rebuild the selector 206 | newSelector := parseSelector(copied.selector) 207 | copied.selector = newSelector 208 | } 209 | 210 | return copied 211 | } 212 | 213 | /* 214 | Check if the specific UI object exists 215 | */ 216 | func (ele Element) WaitForExists(duration float32, maxRetry int) error { 217 | if duration < 0 || duration > 60 { 218 | duration = WAIT_FOR_EXISTS_DURATION 219 | } 220 | 221 | if maxRetry < 0 || maxRetry > 10 { 222 | maxRetry = WAIT_FOR_EXISTS_MAX_RETRY 223 | } 224 | 225 | return ele.wait(duration, maxRetry, true) 226 | } 227 | 228 | /* 229 | Wait the specific UI object disappear 230 | */ 231 | func (ele Element) WaitUntilGone(duration float32, maxRetry int) error { 232 | if duration < 0 || duration > 60 { 233 | duration = WAIT_FOR_DISAPPEAR_DURATION 234 | } 235 | 236 | if maxRetry < 0 || maxRetry > 10 { 237 | maxRetry = WAIT_FOR_DISAPPEAR_MAX_RETRY 238 | } 239 | 240 | return ele.wait(duration, maxRetry, false) 241 | } 242 | 243 | /* 244 | Wait element exists or gone 245 | */ 246 | func (ele Element) wait(duration float32, maxRetry int, exists bool) error { 247 | var ( 248 | err error 249 | retry int 250 | method string 251 | ) 252 | 253 | config := ele.ua.GetConfig() 254 | 255 | if exists { 256 | method = "waitForExists" 257 | } else { 258 | method = "waitUntilGone" 259 | } 260 | 261 | for { 262 | var RPCReturned struct { 263 | Result bool `json:"result"` 264 | } 265 | transform := func(response *http.Response) error { 266 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 267 | if err != nil { 268 | return err 269 | } 270 | return nil 271 | } 272 | 273 | err = ele.ua.post( 274 | &RPCOptions{ 275 | Method: method, 276 | Params: []interface{}{getParams(ele.selector), config.Timeout * 1000}, 277 | }, 278 | nil, 279 | transform, 280 | ) 281 | 282 | if err != nil || RPCReturned.Result == false { 283 | retry++ 284 | 285 | if retry < maxRetry { 286 | time.Sleep(time.Duration(duration*1000) * time.Millisecond) 287 | continue 288 | } 289 | 290 | err = &UiaError{ 291 | Code: -32002, 292 | Message: "Element not found", 293 | } 294 | 295 | break 296 | } 297 | 298 | // It's ok 299 | break 300 | } 301 | 302 | return err 303 | } 304 | 305 | /* 306 | Swipe the element 307 | */ 308 | func (ele Element) swipe(direction string) error { 309 | config := ele.ua.GetConfig() 310 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 311 | return err 312 | } 313 | rect, err := ele.GetRect() 314 | if err != nil { 315 | return err 316 | } 317 | 318 | lx, ly, rx, ry := rect.Left, rect.Top, rect.Right, rect.Bottom 319 | cx, cy := (lx+rx)/2, (ly+ry)/2 320 | 321 | switch direction { 322 | case "up": 323 | return ele.ua.Swipe( 324 | &Position{X: float32(cx), Y: float32(cy)}, 325 | &Position{X: float32(cx), Y: float32(ly)}, 326 | 20, 327 | ) 328 | case "down": 329 | return ele.ua.Swipe( 330 | &Position{X: float32(cx), Y: float32(cy)}, 331 | &Position{X: float32(cx), Y: float32(ry - 1)}, 332 | 20, 333 | ) 334 | case "left": 335 | return ele.ua.Swipe( 336 | &Position{X: float32(cx), Y: float32(cy)}, 337 | &Position{X: float32(lx), Y: float32(cy)}, 338 | 20, 339 | ) 340 | case "right": 341 | return ele.ua.Swipe( 342 | &Position{X: float32(cx), Y: float32(cy)}, 343 | &Position{X: float32(rx - 1), Y: float32(cy)}, 344 | 20, 345 | ) 346 | } 347 | 348 | return nil 349 | } 350 | 351 | /* 352 | Swipe to up 353 | */ 354 | func (ele *Element) SwipeUp() error { 355 | return ele.swipe("up") 356 | } 357 | 358 | /* 359 | Swipe to down 360 | */ 361 | func (ele *Element) SwipeDown() error { 362 | return ele.swipe("down") 363 | } 364 | 365 | /* 366 | Swipe to left 367 | */ 368 | func (ele *Element) SwipeLeft() error { 369 | return ele.swipe("left") 370 | } 371 | 372 | /* 373 | Swipe to right 374 | */ 375 | func (ele *Element) SwipeRight() error { 376 | return ele.swipe("right") 377 | } 378 | 379 | /* 380 | Click on the screen 381 | */ 382 | func (ele *Element) Click(offset *Position) error { 383 | config := ele.ua.GetConfig() 384 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 385 | return err 386 | } 387 | 388 | return ele.ClickNoWait(offset) 389 | } 390 | 391 | func (ele *Element) ClickNoWait(offset *Position) error { 392 | abs, err := ele.Center(offset) 393 | if err != nil { 394 | return err 395 | } 396 | 397 | return ele.ua.Click(abs) 398 | } 399 | 400 | /* 401 | Screen scroll up 402 | */ 403 | func (ele *Element) ScrollUp(step int) error { 404 | if err := ele.ua.post( 405 | &RPCOptions{ 406 | Method: "scrollForward", 407 | Params: []interface{}{ele.selector, true, step}, 408 | }, 409 | nil, 410 | nil, 411 | ); err != nil { 412 | return err 413 | } 414 | 415 | return nil 416 | } 417 | 418 | /* 419 | Screen scroll down 420 | */ 421 | func (ele *Element) ScrollDown(step int) error { 422 | if err := ele.ua.post( 423 | &RPCOptions{ 424 | Method: "scrollBackward", 425 | Params: []interface{}{ele.selector, true, step}, 426 | }, 427 | nil, 428 | nil, 429 | ); err != nil { 430 | return err 431 | } 432 | 433 | return nil 434 | } 435 | 436 | /* 437 | Screen scroll to beginning 438 | */ 439 | func (ele *Element) ScrollToBeginning() error { 440 | if err := ele.ua.post( 441 | &RPCOptions{ 442 | Method: "flingBackward", 443 | Params: []interface{}{ele.selector, true}, 444 | }, 445 | nil, 446 | nil, 447 | ); err != nil { 448 | return err 449 | } 450 | 451 | return nil 452 | } 453 | 454 | /* 455 | Screen scroll to end 456 | */ 457 | func (ele *Element) ScrollToEnd() error { 458 | if err := ele.ua.post( 459 | &RPCOptions{ 460 | Method: "scrollToEnd", 461 | Params: []interface{}{ele.selector, true, 500, 20}, 462 | }, 463 | nil, 464 | nil, 465 | ); err != nil { 466 | return err 467 | } 468 | 469 | return nil 470 | } 471 | 472 | /* 473 | Screen scroll to selector 474 | */ 475 | func (ele *Element) ScrollTo(selector Selector) error { 476 | selector = parseSelector(selector) 477 | 478 | if err := ele.ua.post( 479 | &RPCOptions{ 480 | Method: "scrollTo", 481 | Params: []interface{}{ele.selector, selector, true}, 482 | }, 483 | nil, 484 | nil, 485 | ); err != nil { 486 | return err 487 | } 488 | 489 | return nil 490 | } 491 | 492 | /* 493 | Long click on the element 494 | */ 495 | func (ele *Element) LongClick() error { 496 | config := ele.ua.GetConfig() 497 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 498 | return err 499 | } 500 | 501 | abs, err := ele.Center(nil) 502 | if err != nil { 503 | return err 504 | } 505 | 506 | return ele.ua.LongClick(abs, 0) 507 | } 508 | 509 | /* 510 | Get the children or grandchildren 511 | */ 512 | func (ele Element) Child(selector Selector) *Element { 513 | copied := ele.Clone() 514 | 515 | selector = parseSelector(selector) 516 | 517 | var ( 518 | childOrSibling = copied.selector["childOrSibling"] 519 | childOrSiblingSelector = copied.selector["childOrSiblingSelector"] 520 | ) 521 | 522 | if childOrSibling == nil { 523 | childOrSibling = make([]interface{}, 1) 524 | childOrSibling = append(childOrSibling.([]interface{}), "child") 525 | } 526 | 527 | if childOrSiblingSelector == nil { 528 | childOrSiblingSelector = make([]interface{}, 1) 529 | childOrSiblingSelector = append(childOrSiblingSelector.([]interface{}), selector) 530 | } 531 | 532 | childOrSibling = append(childOrSibling.([]interface{}), "child") 533 | childOrSiblingSelector = append(childOrSiblingSelector.([]interface{}), selector) 534 | 535 | copied.selector["childOrSibling"] = childOrSibling 536 | copied.selector["childOrSiblingSelector"] = childOrSiblingSelector 537 | return copied 538 | } 539 | 540 | func (ele *Element) childByMethod(keywords string, method string, selector Selector) (*Element, error) { 541 | var RPCReturned struct { 542 | Result string `json:"result"` 543 | } 544 | transform := func(response *http.Response) error { 545 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 546 | if err != nil { 547 | return err 548 | } 549 | return nil 550 | } 551 | 552 | selector = parseSelector(selector) 553 | 554 | if err := ele.ua.post( 555 | &RPCOptions{ 556 | Method: method, 557 | Params: []interface{}{ele.selector, selector, keywords, true}, 558 | }, 559 | nil, 560 | transform, 561 | ); err != nil { 562 | return nil, err 563 | } 564 | 565 | ele.selector = map[string]interface{}{"__UID": RPCReturned.Result} 566 | return ele, nil 567 | } 568 | 569 | func (ele *Element) ChildByText(keywords string, selector Selector) (*Element, error) { 570 | return ele.childByMethod(keywords, "childByText", selector) 571 | } 572 | 573 | func (ele *Element) ChildByDescription(keywords string, selector Selector) (*Element, error) { 574 | return ele.childByMethod(keywords, "childByDescription", selector) 575 | } 576 | 577 | /* 578 | Get the sibling 579 | */ 580 | func (ele *Element) Sibling(selector Selector) (*Element, error) { 581 | selector = parseSelector(selector) 582 | 583 | ele.selector["childOrSibling"] = []interface{}{"sibling"} 584 | ele.selector["childOrSiblingSelector"] = []interface{}{selector} 585 | 586 | return ele, nil 587 | } 588 | 589 | /* 590 | Get widget text 591 | */ 592 | func (ele Element) GetText() (string, error) { 593 | config := ele.ua.GetConfig() 594 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 595 | return "", err 596 | } 597 | 598 | return ele.GetTextNoWait() 599 | } 600 | 601 | func (ele Element) GetTextNoWait() (string, error) { 602 | var RPCReturned struct { 603 | Result string `json:"result"` 604 | } 605 | transform := func(response *http.Response) error { 606 | err := json.NewDecoder(response.Body).Decode(&RPCReturned) 607 | if err != nil { 608 | return err 609 | } 610 | return nil 611 | } 612 | 613 | return RPCReturned.Result, ele.ua.post( 614 | &RPCOptions{ 615 | Method: "getText", 616 | Params: []interface{}{getParams(ele.selector)}, 617 | }, 618 | nil, 619 | transform, 620 | ) 621 | } 622 | 623 | /* 624 | Set widget text 625 | */ 626 | func (ele Element) SetText(text string) error { 627 | config := ele.ua.GetConfig() 628 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 629 | return err 630 | } 631 | 632 | return ele.ua.post( 633 | &RPCOptions{ 634 | Method: "setText", 635 | Params: []interface{}{getParams(ele.selector), text}, 636 | }, 637 | nil, 638 | nil, 639 | ) 640 | } 641 | 642 | /* 643 | Clear the widget text 644 | */ 645 | func (ele Element) ClearText() error { 646 | config := ele.ua.GetConfig() 647 | if err := ele.WaitForExists(config.WaitForExistsDuration, config.WaitForExistsMaxRetry); err != nil { 648 | return err 649 | } 650 | 651 | return ele.ua.post( 652 | &RPCOptions{ 653 | Method: "clearTextField", 654 | Params: []interface{}{getParams(ele.selector)}, 655 | }, 656 | nil, 657 | nil, 658 | ) 659 | } 660 | 661 | /* 662 | Query the UI element by selector 663 | */ 664 | func (ua *UIAutomator) GetElementBySelector(selector Selector) (ele *Element) { 665 | ele = &Element{ua: ua} 666 | 667 | selector = parseSelector(selector) 668 | 669 | ele.selector = selector 670 | return 671 | } 672 | 673 | func parseSelector(selector Selector) Selector { 674 | res := make(Selector) 675 | 676 | // Params initalization 677 | res["mask"] = selector["mask"] 678 | res["childOrSibling"] = selector["childOrSibling"] 679 | res["childOrSiblingSelector"] = selector["childOrSiblingSelector"] 680 | 681 | if res["mask"] == nil { 682 | res["mask"] = 0 683 | } 684 | 685 | if res["childOrSibling"] == nil { 686 | res["childOrSibling"] = []interface{}{} 687 | } 688 | 689 | if res["childOrSiblingSelector"] == nil { 690 | res["childOrSiblingSelector"] = []interface{}{} 691 | } 692 | 693 | for k, v := range selector { 694 | if selectorMask, ok := _MASK[k]; ok { 695 | res[k] = v 696 | res["mask"] = res["mask"].(int) | selectorMask 697 | } 698 | } 699 | 700 | return res 701 | } 702 | 703 | func getParams(selector Selector) interface{} { 704 | if uid, ok := selector["__UID"]; ok { 705 | return uid 706 | } 707 | return selector 708 | } 709 | -------------------------------------------------------------------------------- /shell.go: -------------------------------------------------------------------------------- 1 | package uiautomator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func (ua *UIAutomator) Shell(command []string, timeout int) (output string, err error) { 13 | requestURL := fmt.Sprintf("http://%s:%d/shell", ua.config.Host, ua.config.Port) 14 | response, err := http.PostForm( 15 | requestURL, 16 | url.Values{ 17 | "command": {strings.Join(command, " ")}, 18 | "timeout": {strconv.Itoa(timeout)}, 19 | }, 20 | ) 21 | if err != nil { 22 | return 23 | } 24 | 25 | if response.StatusCode != http.StatusOK { 26 | err = boom(response) 27 | return 28 | } 29 | 30 | var ShellReturned struct { 31 | ExitCode int `json:"exitCode"` 32 | Output string `json:"output"` 33 | } 34 | 35 | err = json.NewDecoder(response.Body).Decode(&ShellReturned) 36 | if err != nil { 37 | return 38 | } 39 | if ShellReturned.ExitCode != 0 { 40 | err = &UiaError{ 41 | Code: ShellReturned.ExitCode, 42 | Message: fmt.Sprint("Failed to execute command: %s", command), 43 | } 44 | 45 | return 46 | } 47 | 48 | output = ShellReturned.Output 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /toast.go: -------------------------------------------------------------------------------- 1 | /** 2 | https://github.com/openatx/uiautomator2#toast 3 | */ 4 | package uiautomator 5 | 6 | type Toast struct { 7 | ua *UIAutomator 8 | cached string 9 | } 10 | 11 | /* 12 | Get the toast message 13 | TODO: method "getLastToast" not work 14 | */ 15 | func (t *Toast) GetMessage(timeout float32, cachedTime float32, fallback string) (string, error) { 16 | return fallback, nil 17 | } 18 | 19 | /* 20 | Reset the toast cache 21 | TODO: method "getLastToast" not work 22 | */ 23 | func (t *Toast) Reset(message string, duration float32) error { 24 | return nil 25 | } 26 | 27 | /* 28 | Show toast 29 | */ 30 | func (t *Toast) Show(message string, duration float32) error { 31 | return t.ua.post( 32 | &RPCOptions{ 33 | Method: "makeToast", 34 | Params: []interface{}{message, duration * 1000}, 35 | }, 36 | nil, 37 | nil, 38 | ) 39 | } 40 | 41 | /* 42 | Create toast 43 | */ 44 | func (ua *UIAutomator) NewToast() *Toast { 45 | return &Toast{ua: ua} 46 | } 47 | -------------------------------------------------------------------------------- /uiautomator.go: -------------------------------------------------------------------------------- 1 | package uiautomator 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | const ( 17 | VERSION = "0.0.1" 18 | BASE_URL = "/jsonrpc/0" 19 | 20 | TIMEOUT = 30 // Default timeout(second) 21 | AUTO_RETRY = 5 // Default retry times 22 | RETRY_DURATION = 3 // Default retry duration 23 | WAIT_FOR_EXISTS_MAX_RETRY = 3 // Default WaitForExistsMaxRetry 24 | WAIT_FOR_EXISTS_DURATION = 0.3 // Default WaitForExistsDuration 25 | WAIT_FOR_DISAPPEAR_MAX_RETRY = 3 // Default WaitForDisappearMaxRetry 26 | WAIT_FOR_DISAPPEAR_DURATION = 0.3 // Default WaitForDisappearDuration 27 | ) 28 | 29 | type ( 30 | RPCOptions struct { 31 | URL string 32 | Method string 33 | Params []interface{} 34 | } 35 | 36 | UIAutomator struct { 37 | config *Config 38 | http *http.Client 39 | retryTimes int 40 | size *WindowSize 41 | } 42 | 43 | Config struct { 44 | Host string // Server host 45 | Port int // Server port 46 | Timeout int // Timeout(second) 47 | AutoRetry int // Auto retry times, 0 is without retry 48 | RetryDuration int // Retry duration(second) 49 | WaitForExistsDuration float32 // Unit second 50 | WaitForExistsMaxRetry int // Max retry times 51 | WaitForDisappearDuration float32 // Unit second 52 | WaitForDisappearMaxRetry int // Max retry times 53 | } 54 | ) 55 | 56 | func New(config *Config) *UIAutomator { 57 | if config == nil { 58 | panic("New: config can not be null") 59 | } 60 | 61 | address := net.ParseIP(config.Host) 62 | if address == nil { 63 | errMessage := fmt.Sprintf("Incorrect Config.Host: %s", config.Host) 64 | panic(errMessage) 65 | } else { 66 | config.Host = address.String() 67 | } 68 | 69 | if config.Port <= 0 || config.Port >= 65535 { 70 | errMessage := fmt.Sprintf("Incorrect Config.Port: %d", config.Port) 71 | panic(errMessage) 72 | } 73 | 74 | if config.Timeout < 0 || config.Timeout > 60 { 75 | config.Timeout = TIMEOUT 76 | } 77 | 78 | if config.AutoRetry < 0 || config.AutoRetry > 10 { 79 | config.AutoRetry = AUTO_RETRY 80 | } 81 | 82 | if config.RetryDuration < 0 || config.RetryDuration > 60 { 83 | config.RetryDuration = RETRY_DURATION 84 | } 85 | 86 | if config.WaitForExistsDuration < 0 || config.WaitForExistsDuration > 60 { 87 | config.WaitForExistsDuration = WAIT_FOR_EXISTS_DURATION 88 | } 89 | 90 | if config.WaitForExistsMaxRetry < 0 || config.WaitForExistsMaxRetry > 10 { 91 | config.WaitForExistsMaxRetry = WAIT_FOR_EXISTS_MAX_RETRY 92 | } 93 | 94 | if config.WaitForDisappearDuration < 0 || config.WaitForDisappearDuration > 60 { 95 | config.WaitForDisappearDuration = WAIT_FOR_DISAPPEAR_DURATION 96 | } 97 | 98 | if config.WaitForDisappearMaxRetry < 0 || config.WaitForDisappearMaxRetry > 10 { 99 | config.WaitForDisappearMaxRetry = WAIT_FOR_DISAPPEAR_MAX_RETRY 100 | } 101 | 102 | return &UIAutomator{ 103 | config: config, 104 | http: &http.Client{ 105 | Timeout: time.Duration(config.Timeout) * time.Second, 106 | }, 107 | retryTimes: 0, 108 | } 109 | } 110 | 111 | func (ua UIAutomator) GetConfig() *Config { 112 | return ua.config 113 | } 114 | 115 | func (ua *UIAutomator) Ping() (status string, err error) { 116 | transform := func(response *http.Response) error { 117 | responseBody, err := ioutil.ReadAll(response.Body) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | status = string(responseBody) 123 | return nil 124 | } 125 | 126 | err = ua.get( 127 | &RPCOptions{ 128 | URL: "/ping", 129 | }, 130 | nil, 131 | transform, 132 | ) 133 | 134 | if err != nil { 135 | return 136 | } 137 | 138 | return 139 | } 140 | 141 | func (ua *UIAutomator) caniRetry(err error) bool { 142 | shouldRetry := true && 143 | // Auto retry time should not 0 144 | ua.config.AutoRetry > 0 && 145 | // Retry duration should not 0 146 | ua.config.RetryDuration > 0 && 147 | // Retry time should be less than max auto retry times 148 | ua.retryTimes < ua.config.AutoRetry 149 | 150 | if shouldRetry { 151 | switch err := err.(type) { 152 | case net.Error: 153 | if err.Timeout() { 154 | return true 155 | } 156 | 157 | case *url.Error: 158 | if err.Timeout() { 159 | return true 160 | } 161 | } 162 | } 163 | 164 | return false 165 | } 166 | 167 | func (ua *UIAutomator) execute(request *http.Request, result interface{}, transform interface{}) error { 168 | for { 169 | request.Header.Set("Content-Type", "application/json; charset=utf-8") 170 | request.Header.Set("User-Agent", "UIAUTOMATOR/"+VERSION) 171 | 172 | response, err := ua.http.Do(request) 173 | if err != nil { 174 | if ua.caniRetry(err) { 175 | time.Sleep(time.Duration(ua.config.RetryDuration) * time.Second) 176 | ua.retryTimes++ 177 | continue 178 | } 179 | return err 180 | } 181 | defer response.Body.Close() 182 | 183 | if response.StatusCode != http.StatusOK { 184 | return boom(response) 185 | } 186 | 187 | // Bypass the body parser 188 | if transform != nil { 189 | switch fn := transform.(type) { 190 | case func(interface{}, *http.Response) error: 191 | // Pass 192 | case func(*http.Response) error: 193 | return fn(response) 194 | default: 195 | // Inavlid transform 196 | transform = nil 197 | } 198 | } 199 | 200 | payload, err := parse(response) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // Everything is ok 206 | if transform != nil { 207 | return transform.(func(interface{}, *http.Response) error)(payload, response) 208 | } else { 209 | // Decode the JSON result 210 | if result != nil { 211 | rawJson, err := json.Marshal(payload) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | if err = json.NewDecoder(bytes.NewBuffer(rawJson)).Decode(&result); err != nil { 217 | return err 218 | } 219 | } 220 | } 221 | break 222 | } 223 | 224 | return nil 225 | } 226 | 227 | func (ua *UIAutomator) post(options *RPCOptions, result interface{}, transform interface{}) error { 228 | requestURL := fmt.Sprintf("http://%s:%d%s", ua.config.Host, ua.config.Port, BASE_URL) 229 | payload := struct { 230 | Jsonrpc string `json:"jsonrpc"` 231 | ID string `json:"id"` 232 | Method string `json:"method"` 233 | Params []interface{} `json:"params"` 234 | }{ 235 | Jsonrpc: "2.0", 236 | ID: func() string { 237 | text := fmt.Sprintf("%s at %u", options.Method, time.Now().Unix()) 238 | hasher := md5.New() 239 | hasher.Write([]byte(text)) 240 | return hex.EncodeToString(hasher.Sum(nil)) 241 | }(), 242 | Method: options.Method, 243 | Params: options.Params, 244 | } 245 | 246 | data, err := json.Marshal(payload) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | request, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewBuffer(data)) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | return ua.execute(request, result, transform) 257 | } 258 | 259 | func (ua *UIAutomator) get(options *RPCOptions, result interface{}, transform interface{}) error { 260 | requestURL := fmt.Sprintf("http://%s:%d/%s", ua.config.Host, ua.config.Port, options.URL) 261 | 262 | request, err := http.NewRequest(http.MethodGet, requestURL, nil) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | return ua.execute(request, result, transform) 268 | } 269 | 270 | func parse(response *http.Response) (payload interface{}, err error) { 271 | var RPCReturned struct { 272 | Error *UiaError `json:"error"` 273 | Result interface{} `json:"result"` 274 | } 275 | 276 | responseBody, err := ioutil.ReadAll(response.Body) 277 | if err != nil { 278 | return 279 | } 280 | 281 | if response.Header.Get("Content-Type") != "application/json" { 282 | // Not an json result use the raw data 283 | payload = responseBody 284 | return 285 | } 286 | 287 | if len(responseBody) == 0 { 288 | err = fmt.Errorf("%s - empty body", http.StatusText(response.StatusCode)) 289 | return 290 | } 291 | 292 | err = json.NewDecoder(bytes.NewBuffer(responseBody)).Decode(&RPCReturned) 293 | if err != nil { 294 | return 295 | } 296 | 297 | if RPCReturned.Error != nil { 298 | err = RPCReturned.Error 299 | return 300 | } 301 | 302 | payload = RPCReturned.Result 303 | return 304 | } 305 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | /** 2 | https://github.com/openatx/uiautomator2#watcher 3 | */ 4 | package uiautomator 5 | 6 | type Watcher struct { 7 | name string 8 | ua *UIAutomator 9 | selectors []interface{} 10 | } 11 | 12 | /* 13 | Create a watcher 14 | */ 15 | func (ua *UIAutomator) Watchman() *Watcher { 16 | return &Watcher{ 17 | ua: ua, 18 | selectors: make([]interface{}, 0), 19 | } 20 | } 21 | 22 | /* 23 | Remove watcher 24 | */ 25 | func (watcher *Watcher) Remove(name string) *Watcher { 26 | watcher.ua.post( 27 | &RPCOptions{ 28 | Method: "removeWatcher", 29 | Params: []interface{}{name}, 30 | }, 31 | nil, 32 | nil, 33 | ) 34 | 35 | return watcher 36 | } 37 | 38 | /* 39 | Add trigger condition 40 | */ 41 | func (watcher *Watcher) Register(name string, selector Selector) *Watcher { 42 | watcher.name = name 43 | watcher.selectors = append(watcher.selectors, parseSelector(selector)) 44 | return watcher 45 | } 46 | 47 | /* 48 | Listener has triggered and click the target 49 | */ 50 | func (watcher *Watcher) Click(selector Selector) error { 51 | return watcher.ua.post( 52 | &RPCOptions{ 53 | Method: "registerClickUiObjectWatcher", 54 | Params: []interface{}{watcher.name, watcher.selectors, parseSelector(selector)}, 55 | }, 56 | nil, 57 | nil, 58 | ) 59 | } 60 | --------------------------------------------------------------------------------