├── .directory ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── activity ├── activity.go └── doc.go ├── adbutility ├── adbutility.go └── doc.go ├── device ├── device.go └── doc.go ├── display └── display.go ├── doc.go ├── geometry └── geometry.go ├── goandroid.go ├── goandroid_logo.png ├── input ├── doc.go ├── gesture.go ├── input.go ├── key.go ├── text.go └── touchscreen.go └── view ├── descriptionoperations.go ├── deviceview.go ├── hierarchy.go ├── resourceoperations.go ├── textoperations.go ├── typeoperations.go └── view.go /.directory: -------------------------------------------------------------------------------- 1 | [Dolphin] 2 | Timestamp=2015,6,25,23,24,10 3 | Version=3 4 | 5 | [Settings] 6 | HiddenFilesShown=true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | 5 | env: 6 | global: 7 | - ANDROID_SDK_VERSION="r24.3.3" 8 | - DEBUG_LOG=VV 9 | - ANDROID_TARGET=android-21 10 | - ANDROID_ABI=armeabi-v7a 11 | 12 | before_install: 13 | - go get github.com/modocache/gover 14 | - go get github.com/mattn/goveralls 15 | - go get golang.org/x/tools/cmd/cover 16 | - sudo apt-get update -qq 17 | - if [ `uname -m` = x86_64 ]; then sudo apt-get update; fi 18 | - if [ `uname -m` = x86_64 ]; then sudo apt-get install -qq --force-yes libgd2-xpm ia32-libs ia32-libs-multiarch; fi 19 | - wget http://dl.google.com/android/android-sdk_${ANDROID_SDK_VERSION}-linux.tgz 20 | - tar -zxf android-sdk_${ANDROID_SDK_VERSION}-linux.tgz 21 | - export ANDROID_HOME=`pwd`/android-sdk-linux 22 | - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools 23 | - echo "sdk.dir=$ANDROID_HOME" > local.properties 24 | - echo yes | android update sdk --filter $ANDROID_TARGET,sys-img-$ANDROID_ABI-$ANDROID_TARGET,tools,platform-tools --no-ui --force --all 25 | - echo no | android create avd --force -n emu1 -t $ANDROID_TARGET --abi $ANDROID_ABI 26 | - emulator -avd emu1 -no-skin -no-audio -no-window & 27 | 28 | before_script: 29 | - adb devices 30 | - sleep 60 31 | - adb devices 32 | 33 | script: 34 | - go test -coverprofile=goandroid.coverprofile 35 | - go test -coverprofile=activity.coverprofile ./activity 36 | - go test -coverprofile=adbutility.coverprofile ./adbutility 37 | - go test -coverprofile=device.coverprofile ./device 38 | - go test -coverprofile=display.coverprofile ./display 39 | - go test -coverprofile=geometry.coverprofile ./geometry 40 | - go test -coverprofile=input.coverprofile ./input 41 | - go test -coverprofile=view.coverprofile ./view 42 | - $HOME/gopath/bin/gover 43 | - $HOME/gopath/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kunal Dawn (kunal.dawn@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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![goandroid_logo](goandroid_logo.png) 2 | 3 | goandroid 4 | ========= 5 | [![Project Status](http://stillmaintained.com/kunaldawn/goandroid.png)](https://stillmaintained.com/kunaldawn/goandroid) [![Build Status](http://img.shields.io/travis/kunaldawn/goandroid.svg?style=flat-square)](https://travis-ci.org/kunaldawn/goandroid) [![Coverage Status](http://img.shields.io/coveralls/kunaldawn/goandroid.svg?style=flat-square)](https://coveralls.io/r/kunaldawn/goandroid) [![Issues](http://img.shields.io/github/issues/kunaldawn/goandroid.svg?style=flat-square)](https://github.com/kunaldawn/goandroid/issues) [![Documentation](http://img.shields.io/badge/go-Documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/kunaldawn/goandroid) [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/kunaldawn/goandroid/blob/master/LICENSE) 6 | 7 | Introduction 8 | ------------ 9 | **"goandroid" is an Android automation library purely written in GO.** 10 | 11 | **Project Status :** Under Development, not yet ready for v1.0 release. 12 | 13 | Whether you are an android developer and want to do some automation tasks on your android device to reduce some manual human work, or an enthusiastic developer who want to do some automation taks on your android device, this library allows you to write automation code and do cool stuffs on your android device. 14 | 15 | #### TODO's 16 | - Complete all Package Documentation's 17 | - Implement all features for v1.0 18 | - Write test code for all packages such that it can be tested on Travis CI over emulator 19 | 20 | #### FAQ's 21 | - **Is it a wrapper arround Android [UI Automator](https://developer.android.com/tools/testing-support-library/index.html#UIAutomator) library?** 22 | 23 | No, goandroid does not uses UI Automator for its implementation. It does not uses any java backend or JSON RPC service to communicate UI Automator. 24 | 25 | - **Is it Android testing framework?** 26 | 27 | No, goandroid is not an android trsting framework. Its an automation framework, but can be used for android UI automation and testing perpuses also. 28 | 29 | - **Does it installs anything on my android device?** 30 | 31 | No, goandroid does not installs any APK or other tools in your android device to provide any features. 32 | 33 | - **Can you explain how does it work?** 34 | 35 | This library is purely based on ADB (Android Debugger Bridge) and adb tool. It uses adb commands to perform all operations on device. You can write automation code using this library and check logs for what adb command has been executed for that automation logic. 36 | 37 | - **I want this feature X, can you include X in goandroid?** 38 | 39 | If the feature you are requesting can be implemented only by using adb commands, yes I will add the feature for you. Just make a pull request or start a new issue describing the adb equivalent implementation for the feature. 40 | 41 | - **What are the dependencies of goandroid?** 42 | 43 | The only dependency of goandroid library is "adb" executable tool. 44 | 45 | Usage & Example 46 | ---------------- 47 | 48 | ### Install adb 49 | First of all make sure you have "adb" tool in your system path. 50 | 51 | For Ubuntu 14.04 or later use following commands to install adb: 52 | ```bash 53 | sudo apt-get update 54 | sudo apt-get install android-tools-adb 55 | ``` 56 | 57 | For other distributions, download Android SDK and "adb" tool can be located inside"platform-tools" direcory. Now add this to your system path using following comands: 58 | ```bash 59 | cd 60 | export PATH=$PATH:$PWD/platform-tools 61 | ``` 62 | 63 | ### Initialize Android device instance 64 | First import "github.com/kunaldawn/goandroid" in your source, and you are ready to write some cool automation code. To interact with an android device, you need to create an android device instance first. Following example shows how to create a new android device instance and enable "Show CPU Usage" settings using automation. Please locate the documentation for package [goandroid](https://godoc.org/github.com/kunaldawn/goandroid) for more information. 65 | 66 | **Example:** 67 | 68 | **[Youtube Screen Cast](https://www.youtube.com/watch?v=vuq2Cq82xJ4)** 69 | 70 | ```go 71 | package main 72 | 73 | import ( 74 | "github.com/kunaldawn/goandroid" 75 | ) 76 | 77 | func main() { 78 | // Creat a new android manager with 60 seconds adb time out and take adb 79 | // executable path from system path. 80 | android_manager := goandroid.GetNewAndroidManager(60, "adb") 81 | 82 | // Create an android device instance with following serial 83 | android := android_manager.GetNewAndroidDevice("emulator-5554") 84 | 85 | // Start settings activity 86 | android.Activity.StartActivity("com.android.settings") 87 | // Wait for settings activity to get focused and displayed on screen 88 | // with 10 seconds timeout 89 | android.Activity.WaitForActivityToFocus("com.android.settings", 10) 90 | 91 | // Scroll down to "About phone" 92 | android.View.ScrollDownToText("About phone", 0, 10) 93 | // Now click "About phone" settings item 94 | android.View.ClickText("About phone", 0, 5) 95 | 96 | // Now scroll down to "Build number" 97 | android.View.ScrollDownToText("Build number", 0, 10) 98 | 99 | // Now for faster click operation, we are going to get the view for "Build number" text 100 | view, _ := android.View.GetViewForText("Build number", 0, 5) 101 | 102 | // Now we will click the text 10 times 103 | for i := 0; i < 10; i++ { 104 | android.Input.TouchScreen.Tap(view.Center.X, view.Center.Y) 105 | } 106 | 107 | // Now go back to main settings page 108 | android.Input.Key.PressBack(1) 109 | // Click developer options 110 | android.View.ClickText("Developer options", 0, 10) 111 | 112 | // Now scroll down to "Show CPU Usage" and enable it 113 | android.View.ScrollDownToMatchingText("show cpu", 0, 10) 114 | android.View.ClickMatchingText("show cpu", 0, 10) 115 | } 116 | ``` 117 | 118 | **Translated adb commands by goandroid for above code:** 119 | ``` 120 | adb : [-s emulator-5554 root] 121 | adb : [-s emulator-5554 wait-for-device] 122 | adb : [-s emulator-5554 shell am start com.android.settings] 123 | adb : [-s emulator-5554 shell dumpsys activity | grep mFocusedActivity:] 124 | adb : [-s emulator-5554 shell uiautomator dump] 125 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 126 | adb : [-s emulator-5554 shell wm size] 127 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 128 | adb : [-s emulator-5554 shell uiautomator dump] 129 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 130 | adb : [-s emulator-5554 shell wm size] 131 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 132 | adb : [-s emulator-5554 shell uiautomator dump] 133 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 134 | adb : [-s emulator-5554 shell wm size] 135 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 136 | adb : [-s emulator-5554 shell uiautomator dump] 137 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 138 | adb : [-s emulator-5554 shell uiautomator dump] 139 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 140 | adb : [-s emulator-5554 shell input tap 369 1643] 141 | adb : [-s emulator-5554 shell uiautomator dump] 142 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 143 | adb : [-s emulator-5554 shell uiautomator dump] 144 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 145 | adb : [-s emulator-5554 shell input tap 188 1684] 146 | adb : [-s emulator-5554 shell input tap 188 1684] 147 | adb : [-s emulator-5554 shell input tap 188 1684] 148 | adb : [-s emulator-5554 shell input tap 188 1684] 149 | adb : [-s emulator-5554 shell input tap 188 1684] 150 | adb : [-s emulator-5554 shell input tap 188 1684] 151 | adb : [-s emulator-5554 shell input tap 188 1684] 152 | adb : [-s emulator-5554 shell input tap 188 1684] 153 | adb : [-s emulator-5554 shell input tap 188 1684] 154 | adb : [-s emulator-5554 shell input tap 188 1684] 155 | adb : [-s emulator-5554 shell input keyevent 4] 156 | adb : [-s emulator-5554 shell uiautomator dump] 157 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 158 | adb : [-s emulator-5554 shell input tap 433 1426] 159 | adb : [-s emulator-5554 shell uiautomator dump] 160 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 161 | adb : [-s emulator-5554 shell wm size] 162 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 163 | adb : [-s emulator-5554 shell uiautomator dump] 164 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 165 | adb : [-s emulator-5554 shell wm size] 166 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 167 | adb : [-s emulator-5554 shell uiautomator dump] 168 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 169 | adb : [-s emulator-5554 shell wm size] 170 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 171 | adb : [-s emulator-5554 shell uiautomator dump] 172 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 173 | adb : [-s emulator-5554 shell wm size] 174 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 175 | adb : [-s emulator-5554 shell uiautomator dump] 176 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 177 | adb : [-s emulator-5554 shell wm size] 178 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 179 | adb : [-s emulator-5554 shell uiautomator dump] 180 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 181 | adb : [-s emulator-5554 shell wm size] 182 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 183 | adb : [-s emulator-5554 shell uiautomator dump] 184 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 185 | adb : [-s emulator-5554 shell wm size] 186 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 187 | adb : [-s emulator-5554 shell uiautomator dump] 188 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 189 | adb : [-s emulator-5554 shell wm size] 190 | adb : [-s emulator-5554 shell input touchscreen swipe 540 1440 540 480 1000] 191 | adb : [-s emulator-5554 shell uiautomator dump] 192 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 193 | adb : [-s emulator-5554 shell uiautomator dump] 194 | adb : [-s emulator-5554 shell cat /storage/sdcard/window_dump.xml] 195 | adb : [-s emulator-5554 shell input tap 229 1677] 196 | ``` -------------------------------------------------------------------------------- /activity/activity.go: -------------------------------------------------------------------------------- 1 | package activity 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/kunaldawn/goandroid/device" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Activity struct represents activity subsystem for associated android device. 12 | type Activity struct { 13 | dev device.Device // Device instance 14 | } 15 | 16 | // NewActivity method gives an initialized instance of activity manager. 17 | // It takes device.Device is a parameter and returns an instance of activity.Activity 18 | // struct. 19 | func NewActivity(dev device.Device) Activity { 20 | return Activity{dev: dev} 21 | } 22 | 23 | // StartActivity method launches a activity on device. See "am start" for more 24 | // more details regarding this command. It takes canonilcal class name as its 25 | // first parameter which defines the package and class name of the activity to 26 | // be launched and a list of accepable options by am start command. This method 27 | // requires root access on device and returns error if root access can not be 28 | // granted. It also returns other adb related errors if something goes wrong. 29 | // 30 | // TODO Check if activity can not be launched or not found or other command 31 | // related outputs. 32 | func (am Activity) StartActivity(canonicalClass string, options ...string) error { 33 | err := am.dev.Root() 34 | if err != nil { 35 | return err 36 | } 37 | if len(options) > 0 { 38 | cmd := []string{} 39 | cmd = append(cmd, "start") 40 | cmd = append(cmd, options...) 41 | cmd = append(cmd, canonicalClass) 42 | _, err := am.dev.Shell("am", cmd...) 43 | if err != nil { 44 | return err 45 | } 46 | } else { 47 | _, err := am.dev.Shell("am", "start", canonicalClass) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | // GetFocusedActivity method provides currently focused activity on device screen. 56 | // It uses adb command "dumpsys acyivity" command to determine focused activity 57 | // and extracts package name from the line "mFocusedActivity". 58 | // It returens error if something went wrong on adb side or the method is unable to 59 | // determine the activity canonical package name. 60 | func (am Activity) GetFocusedActivity() (string, error) { 61 | ret, err := am.dev.Shell("dumpsys", "activity", "|", "grep", "mFocusedActivity:") 62 | if err != nil { 63 | return "", err 64 | } 65 | if !strings.Contains(ret, "mFocusedActivity:") { 66 | return "", errors.New("Unable to detect focused activity") 67 | } 68 | trimmed := strings.TrimSpace(ret) 69 | parts := strings.Split(trimmed, " ") 70 | if len(parts) < 3 { 71 | return "", errors.New("Unable to determine activity canonical package name from line : " + trimmed) 72 | } 73 | return parts[3], nil 74 | } 75 | 76 | // IsActivityFocused method checks if given activity is focused on device 77 | // screen or not. If the currently focused activity canonical package contains 78 | // given name then it returns true, and its not case sensitive in nature. 79 | // It returns error if activity information can not be parsed or somethis else 80 | // went wront in between. 81 | func (am Activity) IsActivityFocused(name string) (bool, error) { 82 | activity, err := am.GetFocusedActivity() 83 | if err != nil { 84 | return false, err 85 | } 86 | if strings.Contains(strings.ToLower(activity), strings.ToLower(name)) { 87 | return true, nil 88 | } 89 | return false, nil 90 | } 91 | 92 | // WaitForActivityToFocus method waits for given activity name to be focused on 93 | // device screen in given timeout period. If activity is not focused within timeout 94 | // then error is returned. Also error is returned if something else went wrong such as 95 | // adb error or current focused activity can not be determined. This method returns 96 | // instantly when given activity is found to be focused on screen. 97 | func (am Activity) WaitForActivityToFocus(name string, timeout int) error { 98 | startTime := time.Now() 99 | for { 100 | currentTime := time.Now() 101 | delta := currentTime.Sub(startTime) 102 | if delta.Seconds() >= float64(timeout) { 103 | break 104 | } 105 | stat, err := am.IsActivityFocused(name) 106 | if err != nil { 107 | return err 108 | } 109 | if stat { 110 | return nil 111 | } 112 | } 113 | return errors.New(fmt.Sprintf("Activity %s is not focused on screen within timeout of %d seconds", name, timeout)) 114 | } 115 | -------------------------------------------------------------------------------- /activity/doc.go: -------------------------------------------------------------------------------- 1 | // Package activity provides useful methods to determine or handle activity 2 | // on associated android device. 3 | // 4 | // Following features are provided by this package : 5 | // * Launch a new activity by canonical class name 6 | // * Get currently focused activites canonical class name 7 | // * Determine if certain activity is focused or not 8 | // * Wait for an activity to get focused on device 9 | package activity 10 | -------------------------------------------------------------------------------- /adbutility/adbutility.go: -------------------------------------------------------------------------------- 1 | package adbutility 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // AdbEndpoint struct defines an adb communication endpoint for an android device. 13 | type AdbEndpoint struct { 14 | ADBPath string // Executable path of adb command 15 | } 16 | 17 | // GetNewAdbEndpoint method returns a new ADBEndpoint instance with specified adb 18 | // executable path. If adb tool is on your sustem path, then just pass "adb", else 19 | // mention the full path for your adb executable. 20 | func GetNewAdbEndpoint(adb string) AdbEndpoint { 21 | return AdbEndpoint{ADBPath: adb} 22 | } 23 | 24 | // AdbEndpointOutput is a struct that holds adb command's stdin + stdout and error if something 25 | // went wrong while executing the command. 26 | type AdbEndpointOutput struct { 27 | Ret string // Output of an adb command execution 28 | Err error // Error of an adb command execution 29 | } 30 | 31 | // AdbEndpointOutputChannel is a channel of AdbEndpointOutput type, and used to get adb command output from 32 | // go routine. 33 | type AdbEndpointOutputChannel chan AdbEndpointOutput 34 | 35 | // Execute an ADB command on this endpoint, it takes integer value timeout, 36 | // which specifies the maximum allowed time to run this command before it gets 37 | // killed and a set of arguments as adb command parameters. Please note that it 38 | // panics in case of process can not be killed after timeout. It returns string 39 | // representing the adb command output including stdout and stderr and error is 40 | // returned if something went wrong. 41 | func (ep AdbEndpoint) Adb(timeout int, args ...string) (string, error) { 42 | log.Println("adb :", args) 43 | cmd := exec.Command(ep.ADBPath, args...) 44 | done := make(AdbEndpointOutputChannel) 45 | go func(done AdbEndpointOutputChannel, cmd *exec.Cmd) { 46 | // Run the command and get combined output for stdout and stderr 47 | ret, err := cmd.CombinedOutput() 48 | // Create a output for command 49 | out := AdbEndpointOutput{Ret: string(ret), Err: err} 50 | // Notify that command has complited 51 | done <- out 52 | }(done, cmd) 53 | select { 54 | case out := <-done: 55 | if strings.Contains(strings.TrimSpace(out.Ret), "device not found") { 56 | return "", errors.New("Device is disconnected") 57 | } 58 | return out.Ret, out.Err 59 | case <-time.After(time.Duration(timeout) * time.Second): 60 | err := cmd.Process.Kill() 61 | if err != nil { 62 | panic(err) 63 | } 64 | return "", errors.New(fmt.Sprintf("Timeout occured after %d sec while executing adb %v", timeout, args)) 65 | } 66 | return "", nil 67 | } 68 | 69 | // GetAttachedDevices method returns list of attached device serial number 70 | // to this local endpoint as a sclice of string. Error is also returned if 71 | // something went wront in adb side. 72 | func (ep AdbEndpoint) GetAttachedDevices(timeout int) ([]string, error) { 73 | devices := []string{} 74 | adb_output, err := ep.Adb(timeout, "devices") 75 | if err != nil { 76 | return devices, err 77 | } 78 | list := strings.Split(adb_output, "\n") 79 | for line := range list { 80 | if strings.Contains(list[line], "device") && !strings.Contains(list[line], "List of") { 81 | device_id_line := strings.Split(list[line], "device") 82 | device_id := strings.TrimSpace(device_id_line[0]) 83 | devices = append(devices, device_id) 84 | } 85 | } 86 | return devices, nil 87 | } 88 | 89 | // WaitForSerial waits for speified set of serial numbers to be available on this 90 | // local endpoint. It returns error in case of all specified serial numbers can not 91 | // be detected by this endpoint. Error is also returned if something went wront in 92 | // adb side. 93 | func (ep AdbEndpoint) WaitForSerials(timeout int, serials ...string) error { 94 | if len(serials) == 0 { 95 | return errors.New(fmt.Sprintf("No serials specified", timeout)) 96 | } 97 | 98 | innter_to := 5 99 | if timeout < innter_to { 100 | innter_to = timeout 101 | } 102 | start := time.Now() 103 | for { 104 | current := time.Now() 105 | delta := current.Sub(start) 106 | if delta.Seconds() >= float64(timeout) { 107 | break 108 | } 109 | devs, err := ep.GetAttachedDevices(innter_to) 110 | if err != nil { 111 | return err 112 | } 113 | found := 0 114 | for _, dev := range devs { 115 | for _, item := range serials { 116 | if dev == item { 117 | found += 1 118 | break 119 | } 120 | } 121 | } 122 | if found == len(serials) { 123 | return nil 124 | } 125 | time.Sleep(time.Second) 126 | } 127 | return errors.New(fmt.Sprintf("Timeout occured after %d sec while waiting for serials", timeout)) 128 | } 129 | 130 | // WaitForDevices method waits for specified number of devices to be available to this 131 | // local endpoint. If specific number of devices can not be located in timeout, it returns 132 | // error. Error is also returned if something went wront in adb side. 133 | func (ep AdbEndpoint) WaitForDevices(timeout int, count int) error { 134 | if count <= 0 { 135 | return errors.New(fmt.Sprintf("Can not wait for less than or eual to zero devices", timeout)) 136 | } 137 | 138 | innter_to := 5 139 | if timeout < innter_to { 140 | innter_to = timeout 141 | } 142 | start := time.Now() 143 | for { 144 | current := time.Now() 145 | delta := current.Sub(start) 146 | if delta.Seconds() >= float64(timeout) { 147 | break 148 | } 149 | devs, err := ep.GetAttachedDevices(innter_to) 150 | if err != nil { 151 | return err 152 | } 153 | if count == len(devs) { 154 | return nil 155 | } 156 | time.Sleep(time.Second) 157 | } 158 | return errors.New(fmt.Sprintf("Timeout occured after %d sec while waiting for devices", timeout)) 159 | } 160 | -------------------------------------------------------------------------------- /adbutility/doc.go: -------------------------------------------------------------------------------- 1 | // Package adbutility provides useful methods to communicate with underlying 2 | // adb tool. All device interactions are done via this package and this is the 3 | // lowest level in the method call hierarchy. 4 | // 5 | // Following are the features provided by this package. 6 | // * Ability to communicate with adb endpoint on local machine 7 | // * Execute adb commands on local machine and get the output back 8 | // * Get list of attached devices serial numbers 9 | // * Wait for a set of serial numbers to be attached to the adb endpoint 10 | // * Wait for a specific number of devices to be attached to the adb endpoint 11 | // * Each adb call is logged for debugging purpose 12 | // * Each adb command is associated with a timeout slot, and the process is automatically killed after timeout. 13 | package adbutility 14 | -------------------------------------------------------------------------------- /device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/kunaldawn/goandroid/adbutility" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | PROP_BOOT_STATUS = "sys.boot_completed" // Property that represents devices boot status 13 | ) 14 | 15 | // Device struct represents adb capable android device, its serial number and 16 | // associated adb endpoint. Device timeout represents all device specific adb operation timeouts. 17 | type Device struct { 18 | Serial string // Device serial number 19 | Timeout int // Timeout in seconds for all adb and shell operations 20 | AdbEndpoint adbutility.AdbEndpoint // Adb endpoint for this device 21 | } 22 | 23 | // NewDevice method creates a new Device based on given serial number, adb operation timeout and 24 | // connecting adb enpoint for this device. 25 | func NewDevice(serial string, timeout int, endPoint adbutility.AdbEndpoint) Device { 26 | return Device{Serial: serial, Timeout: timeout, AdbEndpoint: endPoint} 27 | } 28 | 29 | // IsAvailable method checkes availability of the device in the adb endpoint. It returns 30 | // boolean value indicating availibility status of the device and error in case of 31 | // something went wrong while doing adb operations. 32 | func (dev Device) IsAvailable() (bool, error) { 33 | devices, err := dev.AdbEndpoint.GetAttachedDevices(dev.Timeout) 34 | if err != nil { 35 | return false, err 36 | } 37 | for index := range devices { 38 | if dev.Serial == devices[index] { 39 | return true, nil 40 | } 41 | } 42 | return false, nil 43 | } 44 | 45 | // Adb method allows to execute adb command on this device instance. It takes a adb command 46 | // and optional list of arguments. It returns outout of the adb command and error in case 47 | // of something went wrong. Please note that adb will timeout within default adb operation 48 | // timeout. 49 | func (dev Device) Adb(command string, args ...string) (string, error) { 50 | return dev.AdbEndpoint.Adb(dev.Timeout, append([]string{"-s", dev.Serial, command}, args...)...) 51 | } 52 | 53 | // Shell method allows to execute adb shell commands on associated device instance. It takes 54 | // a shell comand and a list of opetional command arguments. It returns adb shell command output 55 | // and stderr output combiled as string and error in case of adb operation failure or timeout. 56 | func (dev Device) Shell(command string, args ...string) (string, error) { 57 | return dev.Adb("shell", append([]string{command}, args...)...) 58 | } 59 | 60 | // GetProperty method is used to extract a device property value based on the specified 61 | // key. It returns string representation of the property value and error in case of 62 | // adb operation failure or specified key is not located. 63 | func (dev Device) GetProperty(key string) (string, error) { 64 | prop, err := dev.GetAllProperties() 65 | if err != nil { 66 | return "", err 67 | } 68 | val, ok := prop[key] 69 | if !ok { 70 | return "", errors.New(fmt.Sprintf("Key [%s] is not found in device properties", key)) 71 | } 72 | return val, nil 73 | } 74 | 75 | // GetAllProperties method returns a map of [key: value] pairs of all device 76 | // properties. It also returns error in case of adb operation failure. 77 | func (dev Device) GetAllProperties() (map[string]string, error) { 78 | prop_map := make(map[string]string) 79 | prop, err := dev.Shell("getprop") 80 | if err != nil { 81 | return prop_map, err 82 | } 83 | lines := strings.Split(prop, "\n") 84 | for index := range lines { 85 | parts := strings.Split(lines[index], ":") 86 | if len(parts) == 2 { 87 | key := strings.TrimSpace(strings.Replace(strings.Replace(parts[0], "[", "", -1), "]", "", -1)) 88 | value := strings.TrimSpace(strings.Replace(strings.Replace(parts[1], "[", "", -1), "]", "", -1)) 89 | prop_map[key] = value 90 | } 91 | } 92 | return prop_map, nil 93 | } 94 | 95 | // Pull method pulls a file or directory from specified source path to specified 96 | // destination path. Specified path must exist on device and specified destination path 97 | // must exist on local machine. It returns command output and error in case of adb operation 98 | // failed. 99 | func (dev Device) Pull(src string, dst string) (string, error) { 100 | return dev.Adb("pull", src, dst) 101 | } 102 | 103 | // Push method pushes a file or directory from local machine from specified source 104 | // path to device in specified destination path. Specified source path mast exist on 105 | // local machine and destinaltion path mast exist on device. It returns command output 106 | // and error in case of adb operation failed. 107 | func (dev Device) Push(src string, dst string) (string, error) { 108 | return dev.Adb("push", src, dst) 109 | } 110 | 111 | // WaitForAvailability method waits for associatred device to be available or 112 | // attached to the adb endpoint within specified timeout period. It returns 113 | // error in case of adb operation failure or timeout. 114 | func (dev Device) WaitForAvailability(timeout int) error { 115 | _, err := dev.AdbEndpoint.Adb(timeout, "-s", dev.Serial, "wait-for-device") 116 | return err 117 | } 118 | 119 | // Root method gains root access to the device, for this the device must be rooted. 120 | // It returns error in case of root failed or adb operation failed. 121 | func (dev Device) Root() error { 122 | out, err := dev.Adb("root") 123 | if err != nil { 124 | return err 125 | } 126 | if !strings.Contains(out, "restarting adbd as root") && !strings.Contains(out, "adbd is already running as root") { 127 | return errors.New("Unable to gain root access to device") 128 | } 129 | return dev.WaitForAvailability(dev.Timeout) 130 | } 131 | 132 | // Reboot method reboots associated device. It waits for the device to become 133 | // available again within specified restart timeout and the device to complete 134 | // its boot sequence within specified boot timeout. It returns error in case 135 | // of adb operation failure or timeout has been reached. 136 | func (dev Device) Reboot(restartTimeout int, bootTimeout int) error { 137 | _, err := dev.Adb("reboot") 138 | if err != nil { 139 | return err 140 | } 141 | err = dev.WaitForAvailability(restartTimeout) 142 | if err != nil { 143 | return err 144 | } 145 | return dev.WaitForBootToComplete(bootTimeout) 146 | } 147 | 148 | // WaitForBootToComplete method waits for the device to complete its boot sequence 149 | // within specified ammount of timeout. It returns error in case of adb operation failure 150 | // or timeout has been reached. 151 | func (dev Device) WaitForBootToComplete(bootTimeout int) error { 152 | startTime := time.Now() 153 | for { 154 | currentTime := time.Now() 155 | delta := currentTime.Sub(startTime) 156 | if delta.Seconds() >= float64(bootTimeout) { 157 | break 158 | } 159 | val, err := dev.GetProperty(PROP_BOOT_STATUS) 160 | if err != nil { 161 | return err 162 | } 163 | if val == "1" { 164 | return nil 165 | } 166 | } 167 | return errors.New("Device not completed boot sequence in timeout") 168 | } 169 | -------------------------------------------------------------------------------- /device/doc.go: -------------------------------------------------------------------------------- 1 | // Package device provides some useful methods to operate on an associated 2 | // android device. 3 | // 4 | // Following are the features of this package: 5 | // * It provides generic methods to run adb or adb shell command on the associated device 6 | // * Gives ability to check if associated device is available or not 7 | // * Gives ability to gain adb root access to the associated device 8 | // * Gives ability to reboot associated device 9 | // * Gives ablity to wait for different device states 10 | // * Gives ablity to push or pull files/directories to/from device 11 | // * Gives ablity to fetch properties from the device 12 | package device 13 | -------------------------------------------------------------------------------- /display/display.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/kunaldawn/goandroid/device" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Display struct { 12 | dev device.Device 13 | } 14 | 15 | func NewDisplay(dev device.Device) Display { 16 | return Display{dev: dev} 17 | } 18 | 19 | func (disp Display) GetDisplaySize() (int, int, error) { 20 | txt, err := disp.dev.Shell("wm", "size") 21 | if err != nil { 22 | return -1, -1, err 23 | } 24 | if !strings.Contains(txt, "Physical size:") { 25 | return -1, -1, errors.New("Not able to determine display size") 26 | } 27 | size := strings.Split(strings.TrimSpace(txt), "Physical size:")[1] 28 | width, err := strconv.Atoi(strings.Split(strings.TrimSpace(size), "x")[0]) 29 | if err != nil { 30 | return -1, -1, err 31 | } 32 | height, err := strconv.Atoi(strings.Split(strings.TrimSpace(size), "x")[1]) 33 | if err != nil { 34 | return -1, -1, err 35 | } 36 | return width, height, nil 37 | } 38 | 39 | func (disp Display) SetDisplaySize(width int, height int) error { 40 | size := fmt.Sprintf("%dx%d", width, height) 41 | _, err := disp.dev.Shell("wm", "size", size) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package goandroid is an automation library for android devices. 2 | package goandroid 3 | -------------------------------------------------------------------------------- /geometry/geometry.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | type Point struct { 4 | X int // X Coordinate of the point 5 | Y int // Y Coordinate of the point 6 | } 7 | 8 | type Points []Point 9 | 10 | type Rect struct { 11 | TopLeft Point // Top left coordinate of the rectangle 12 | BottomRight Point // Bottom right coordinate of the rectangle 13 | } 14 | -------------------------------------------------------------------------------- /goandroid.go: -------------------------------------------------------------------------------- 1 | package goandroid 2 | 3 | import ( 4 | "github.com/kunaldawn/goandroid/activity" 5 | "github.com/kunaldawn/goandroid/adbutility" 6 | "github.com/kunaldawn/goandroid/device" 7 | "github.com/kunaldawn/goandroid/display" 8 | "github.com/kunaldawn/goandroid/input" 9 | "github.com/kunaldawn/goandroid/view" 10 | ) 11 | 12 | // AndroidManager struct defines a android device manager with an associated 13 | // adb endpoint and adb operation timeout. All devices returned by this android 14 | // manager will be having this adb operation timeout. 15 | type AndroidManager struct { 16 | Endpoint adbutility.AdbEndpoint // Associated adb endpoint 17 | Timeout int // Default adb operation timeout in seconds 18 | } 19 | 20 | // Android struct defines an android device. Android device has raw device 21 | // communication interface via Device struct, device input interaction interface 22 | // via Input struct, device ui view query interface via View struct, device display 23 | // information interface via Display struct and device application activity 24 | // interface via Activity struct. See their respective documentation for 25 | // list of available mechanisms. 26 | type Android struct { 27 | Device device.Device // Raw adb device communication interface 28 | Input input.InputManager // Device input interaction interface 29 | View view.DeviceView // Device UI View query interface 30 | Display display.Display // Device display insterface 31 | Activity activity.Activity // Device application activity interface 32 | } 33 | 34 | // GetAndroidManager method returns a new AndroidManager instance based on specified 35 | // adb executable path and adb operation timeout. Please note that adb executable must 36 | // be present on the specified path. If adb is on system path then just pass "adb". 37 | // Timeout specified is in seconds and all adb commands will timeout after specified 38 | // seconds. 39 | func GetNewAndroidManager(timeout int, adb string) AndroidManager { 40 | return AndroidManager{Endpoint: adbutility.GetNewAdbEndpoint(adb), Timeout: timeout} 41 | } 42 | 43 | // GetNewAndroidDevice method returns a new Android device instance based on the 44 | // specified serial number. 45 | func (am AndroidManager) GetNewAndroidDevice(serial string) Android { 46 | dev := device.NewDevice(serial, am.Timeout, am.Endpoint) 47 | inp := input.NewInputManager(dev) 48 | viw := view.NewDeviceView(dev) 49 | disp := display.NewDisplay(dev) 50 | act := activity.NewActivity(dev) 51 | return Android{dev, inp, viw, disp, act} 52 | } 53 | 54 | // GetAttachedAndroidDevices method returns list of attached android devices 55 | // to the system. It returns error if any error occurred wile performing adb operation. 56 | func (am AndroidManager) GetAttachedAndroidDevices() ([]Android, error) { 57 | serials, err := am.Endpoint.GetAttachedDevices(am.Timeout) 58 | if err != nil { 59 | return []Android{}, err 60 | } 61 | devices := []Android{} 62 | for index := range serials { 63 | dev := am.GetNewAndroidDevice(serials[index]) 64 | devices = append(devices, dev) 65 | } 66 | return devices, nil 67 | } 68 | -------------------------------------------------------------------------------- /goandroid_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kunaldawn/goandroid/028fd47d5f30053146dc0d7ffd593e2d15704bfb/goandroid_logo.png -------------------------------------------------------------------------------- /input/doc.go: -------------------------------------------------------------------------------- 1 | // Package input provides a set of useful methods that helps on input based interaction 2 | // with an associated android device. All input interactions are either based on "adb shell input" 3 | // command or via sending raw input commands to input device directly. 4 | // 5 | // Following are the list of supported input mechanisms : 6 | // * Touch screen based input 7 | // * Key press based input 8 | // * Simple text input 9 | // 10 | // Touch screen based input operations provide following features: 11 | // * Perform single touch on any screen coordinate 12 | // * Draw multi point gesture on real device or emulator 13 | // * Perform swipe operation between any two points in the screen 14 | // * Perform generic swipe operations such as swipe up/down/left/right on screen 15 | // * Determine devices touch input device path 16 | // * Send raw touch input commands to touch input device 17 | // 18 | // Key press based input operations provide following features: 19 | // * Send any key press code for any number of times 20 | // * Provides generic key press methods such as home/power/back... 21 | // 22 | // Text input operations provide following features: 23 | // * Allows you to enter simple text on devices focused input area 24 | // 25 | // Currently text input operation is very limited and can not insert any 26 | // special characters or unicode characters. This limitation is imposed by 27 | // "adb shell input text" command. 28 | package input 29 | -------------------------------------------------------------------------------- /input/gesture.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/kunaldawn/goandroid/geometry" 5 | "time" 6 | ) 7 | 8 | const ( 9 | EV_ABS = 3 // ABS Event 10 | EV_SYN = 0 // Sync Event 11 | EV_KEY = 1 // Key event 12 | BTN_TOUCH = 330 // Touch event 13 | BTN_TOOL_FINGER = 325 // Finger event 14 | DOWN = 1 // Touch down event 15 | UP = 0 // Touch up event 16 | ABS_MT_TRACKING_ID = 57 // ID of the touch (important for multi-touch reports) 17 | ABS_MT_TOUCH_MAJOR = 48 // Touch size in pixels 18 | ABS_MT_POSITION_X = 53 // X coordinate of the touch 19 | ABS_X = 0 // X coordinate of touch in emulator 20 | ABS_MT_POSITION_Y = 54 // Y coordinate of the touch 21 | ABS_Y = 1 // Y coordinate of touch in emulator 22 | ABS_MT_PRESSURE = 58 // Pressure of the touch 23 | SYN_MT_REPORT = 2 // End of separate touch data 24 | SYN_REPORT = 0 // End of report 25 | DEFAULT_TOUCH_ID = 0 // Default touch point id 26 | DEFAULT_PRESSURE = 50 // Touch pressure default value 27 | DEFAULT_FINGER_TIP_SIZE = 5 // Default touch finger tip size 28 | ) 29 | 30 | // DrawGesture method allows you to draw a continious gesture on device view. 31 | // It takes a set of points and a delay parameter, gesture is drawn based on 32 | // given set of points and each point is iterated after specific delay. Please 33 | // note that the delay here is in milliseconds. It returns error on adb 34 | // operation errors. Gesture is drawn based on multitouch protocol v2 events. 35 | func (ts TouchScreen) DrawGesture(points geometry.Points, delay int) error { 36 | dev, err := ts.GetTouchInputDevice() 37 | if err != nil { 38 | return err 39 | } 40 | for index, pt := range points { 41 | if index == 0 { 42 | err = ts.RawSendEvent(dev, EV_KEY, BTN_TOUCH, DOWN) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | err = ts.RawMovePoint(dev, pt.X, pt.Y, DEFAULT_TOUCH_ID, DEFAULT_PRESSURE, DEFAULT_FINGER_TIP_SIZE) 48 | if err != nil { 49 | return err 50 | } 51 | time.Sleep(time.Duration(delay) * time.Millisecond) 52 | } 53 | err = ts.RawSendEvent(dev, EV_ABS, ABS_MT_TRACKING_ID, 4294967295) 54 | if err != nil { 55 | return err 56 | } 57 | err = ts.RawSendEvent(dev, EV_KEY, BTN_TOUCH, UP) 58 | if err != nil { 59 | return err 60 | } 61 | err = ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 62 | if err != nil { 63 | return err 64 | } 65 | err = ts.RawSendEvent(dev, EV_KEY, BTN_TOOL_FINGER, UP) 66 | if err != nil { 67 | return err 68 | } 69 | return ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 70 | } 71 | 72 | // DrawGestureEmulator method allows you to draw gesture on emulator device. 73 | // Emulator devices are generally single touch and operates on different 74 | // touch protocol than of multitouch v2 protocol. This method takes a set of 75 | // points and a delay parameter, gesture is drawn based on given set of points 76 | // and iterated one by one with specific delay in between. Please note tha 77 | // value of delay is in milliseconds. It returns error on adb operation failure. 78 | func (ts TouchScreen) DrawGestureEmulator(points geometry.Points, delay int) error { 79 | dev, err := ts.GetTouchInputDevice() 80 | if err != nil { 81 | return err 82 | } 83 | for index, pt := range points { 84 | err = ts.RawMovePointEmulator(dev, pt.X, pt.Y) 85 | if err != nil { 86 | return err 87 | } 88 | if index == 0 { 89 | err = ts.RawSendEvent(dev, EV_KEY, BTN_TOUCH, DOWN) 90 | if err != nil { 91 | return err 92 | } 93 | err = ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | time.Sleep(time.Duration(delay) * time.Millisecond) 99 | } 100 | err = ts.RawSendEvent(dev, EV_KEY, BTN_TOUCH, UP) 101 | if err != nil { 102 | return err 103 | } 104 | return ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 105 | } 106 | 107 | // RawMovePoint method allows you to move a gesture point on device screen. 108 | // This can be used to draw gesture with different parameters than of defauls. 109 | // Please note that users need to send sync signals by them self, check 110 | // DrawGesture method implementation regarding this, if user specific gesture 111 | // method method implementation is required. This method takes x and y coordinates 112 | // of new gesture point along with touch point id, touch pressure and touch 113 | // finger tip size and touch device path. It returns error on adb operation failure. 114 | func (ts TouchScreen) RawMovePoint(dev string, x int, y int, id int, pressure int, size int) error { 115 | err := ts.RawSendEvent(dev, EV_ABS, ABS_MT_TRACKING_ID, id) 116 | if err != nil { 117 | return err 118 | } 119 | err = ts.RawSendEvent(dev, EV_ABS, ABS_MT_POSITION_X, x) 120 | if err != nil { 121 | return err 122 | } 123 | err = ts.RawSendEvent(dev, EV_ABS, ABS_MT_POSITION_Y, y) 124 | if err != nil { 125 | return err 126 | } 127 | err = ts.RawSendEvent(dev, EV_ABS, ABS_MT_TOUCH_MAJOR, size) 128 | if err != nil { 129 | return err 130 | } 131 | err = ts.RawSendEvent(dev, EV_ABS, ABS_MT_PRESSURE, pressure) 132 | if err != nil { 133 | return err 134 | } 135 | return ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 136 | } 137 | 138 | // RawMovePointEmulator method allows you to move a gesture point on emulator screen. 139 | // This can be used to draw gesture with different parameters than of defauls. 140 | // Please note that users need to send sync signals by them self, check 141 | // DrawGestureEmulator method implementation regarding this, if user specific gesture 142 | // method method implementation is required. This method takes x and y coordinates 143 | // of new gesture point along with touch device path and returns error on adb operation failure. 144 | func (ts TouchScreen) RawMovePointEmulator(dev string, x int, y int) error { 145 | err := ts.RawSendEvent(dev, EV_ABS, ABS_X, x) 146 | if err != nil { 147 | return err 148 | } 149 | err = ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 150 | if err != nil { 151 | return err 152 | } 153 | err = ts.RawSendEvent(dev, EV_ABS, ABS_Y, y) 154 | if err != nil { 155 | return err 156 | } 157 | return ts.RawSendEvent(dev, EV_SYN, SYN_REPORT, 0) 158 | } 159 | -------------------------------------------------------------------------------- /input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/kunaldawn/goandroid/device" 5 | ) 6 | 7 | // InputManager struct holds all type of supported device input interfaces 8 | // such as touch screen input interface to operate touch based inputes, key 9 | // input interface to operate on key press inputs, text input interface to 10 | // insert text on selected text box (text input function is very limited at this 11 | // point, does not supports unicode and any special characters and its limited 12 | // by "adb shell input text capabilites"). 13 | type InputManager struct { 14 | dev device.Device // Associated device 15 | TouchScreen TouchScreen // Associated touch screen input 16 | Key Key // Associated key input 17 | TextInput TextInput // Associated text input 18 | } 19 | 20 | // NewInputManager method returns a new InputManager struct and associates 21 | // it with given device. 22 | func NewInputManager(dev device.Device) InputManager { 23 | ts := NewTouchScreen(dev) 24 | key := NewKey(dev) 25 | ti := NewTextInput(dev) 26 | return InputManager{dev: dev, TouchScreen: ts, Key: key, TextInput: ti} 27 | } 28 | -------------------------------------------------------------------------------- /input/key.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/kunaldawn/goandroid/device" 5 | "strconv" 6 | ) 7 | 8 | const ( 9 | MENU_KEY = 1 // Menu key code 10 | HOME_KEY = 3 // Home key code 11 | BACK_KEY = 4 // Back key code 12 | CALL_KEY = 5 // Cll key code 13 | ENDCALL_KEY = 6 // End call key code 14 | UP_KEY = 19 // Up key code 15 | DOWN_KEY = 20 // Down key code 16 | LEFT_KEY = 21 // Left key code 17 | RIGHT_KEY = 22 // Right key code 18 | VOLUME_UP_KEY = 24 // Volume up key code 19 | VOLUME_DOWN_KEY = 25 // Volume down key code 20 | POWER_KEY = 26 // Power key code 21 | CAMERA_KEY = 27 // Camera key code 22 | ENTER_KEY = 66 // Enter key code 23 | DEL_KEY = 67 // Del key code 24 | ) 25 | 26 | // Key struct defines a key input subsystem associated with a device. 27 | type Key struct { 28 | dev device.Device // Associated device 29 | } 30 | 31 | // NewKey method returns a new Key struct associated with given device. 32 | func NewKey(dev device.Device) Key { 33 | return Key{dev: dev} 34 | } 35 | 36 | // Press method performs a key press event based on given key code, this operation 37 | // is repeated based on the count parameter. It returns error on adb operation 38 | // failure. 39 | func (key Key) Press(code int, count int) error { 40 | for i := 0; i < count; i++ { 41 | _, err := key.dev.Shell("input", "keyevent", strconv.Itoa(code)) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | // PressMenu method performs a menu key press event, this operation is repeated 50 | // based on the count parameter. It returns error on adb operation failure. 51 | func (key Key) PressMenu(count int) error { 52 | return key.Press(MENU_KEY, count) 53 | } 54 | 55 | // PressHome method performs a home key press event, this operation is repeated 56 | // based on the count parameter. It returns error on adb operation failure. 57 | func (key Key) PressHome(count int) error { 58 | return key.Press(HOME_KEY, count) 59 | } 60 | 61 | // PressBack method performs a back key press event, this operation is repeated 62 | // based on the count parameter. It returns error on adb operation failure. 63 | func (key Key) PressBack(count int) error { 64 | return key.Press(BACK_KEY, count) 65 | } 66 | 67 | // PressCall method performs a call key press event, this operation is repeated 68 | // based on the count parameter. It returns error on adb operation failure. 69 | func (key Key) PressCall(count int) error { 70 | return key.Press(CALL_KEY, count) 71 | } 72 | 73 | // PressEndCall method performs a end call key press event, this operation is repeated 74 | // based on the count parameter. It returns error on adb operation failure. 75 | func (key Key) PressEndCall(count int) error { 76 | return key.Press(ENDCALL_KEY, count) 77 | } 78 | 79 | // PressUp method performs a up key press event, this operation is repeated 80 | // based on the count parameter. It returns error on adb operation failure. 81 | func (key Key) PressUp(count int) error { 82 | return key.Press(UP_KEY, count) 83 | } 84 | 85 | // PressDown method performs a down key press event, this operation is repeated 86 | // based on the count parameter. It returns error on adb operation failure. 87 | func (key Key) PressDown(count int) error { 88 | return key.Press(DOWN_KEY, count) 89 | } 90 | 91 | // PressLeft method performs a left key press event, this operation is repeated 92 | // based on the count parameter. It returns error on adb operation failure. 93 | func (key Key) PressLeft(count int) error { 94 | return key.Press(LEFT_KEY, count) 95 | } 96 | 97 | // PressRight method performs a right key press event, this operation is repeated 98 | // based on the count parameter. It returns error on adb operation failure. 99 | func (key Key) PressRight(count int) error { 100 | return key.Press(RIGHT_KEY, count) 101 | } 102 | 103 | // PressVolumeUp method performs a volume up key press event, this operation is repeated 104 | // based on the count parameter. It returns error on adb operation failure. 105 | func (key Key) PressVolumeUp(count int) error { 106 | return key.Press(VOLUME_UP_KEY, count) 107 | } 108 | 109 | // PressVolumeDown method performs a volume down key press event, this operation is repeated 110 | // based on the count parameter. It returns error on adb operation failure. 111 | func (key Key) PressVolumeDown(count int) error { 112 | return key.Press(VOLUME_DOWN_KEY, count) 113 | } 114 | 115 | // PressPower method performs a power key press event, this operation is repeated 116 | // based on the count parameter. It returns error on adb operation failure. 117 | func (key Key) PressPower(count int) error { 118 | return key.Press(POWER_KEY, count) 119 | } 120 | 121 | // PressCamera method performs a camera key press event, this operation is repeated 122 | // based on the count parameter. It returns error on adb operation failure. 123 | func (key Key) PressCamera(count int) error { 124 | return key.Press(CAMERA_KEY, count) 125 | } 126 | 127 | // PressEnter method performs a enter key press event, this operation is repeated 128 | // based on the count parameter. It returns error on adb operation failure. 129 | func (key Key) PressEnter(count int) error { 130 | return key.Press(ENTER_KEY, count) 131 | } 132 | 133 | // PressDelete method performs a delete key press event, this operation is repeated 134 | // based on the count parameter. It returns error on adb operation failure. 135 | func (key Key) PressDelete(count int) error { 136 | return key.Press(DEL_KEY, count) 137 | } 138 | -------------------------------------------------------------------------------- /input/text.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/kunaldawn/goandroid/device" 5 | "strings" 6 | ) 7 | 8 | // TextInput struct represents a text input subsystem for associated device. 9 | type TextInput struct { 10 | dev device.Device // Associated device 11 | } 12 | 13 | // NewTextInput method returns a new TextInput struct which is associated with 14 | // given device. 15 | func NewTextInput(dev device.Device) TextInput { 16 | return TextInput{dev: dev} 17 | } 18 | 19 | // EnterText method enters text on selected input area. Input area must be 20 | // selected previously. Functionality of this method is very limited and 21 | // does not support any unicode aharacters or any special characters. 22 | func (ti TextInput) EnterText(text string) { 23 | formatted := strings.Replace(text, " ", "%s", -1) 24 | ti.dev.Shell("input", "text", formatted) 25 | } 26 | -------------------------------------------------------------------------------- /input/touchscreen.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "errors" 5 | "github.com/kunaldawn/goandroid/device" 6 | "github.com/kunaldawn/goandroid/display" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // TouchScreen struct represensts touch input susbystem for associated device. 12 | type TouchScreen struct { 13 | dev device.Device // Associated device 14 | disp display.Display // Associated device display 15 | } 16 | 17 | // NewTouchScreen method returns a new TouchScreen and associates it with 18 | // given device. 19 | func NewTouchScreen(dev device.Device) TouchScreen { 20 | disp := display.NewDisplay(dev) 21 | return TouchScreen{dev: dev, disp: disp} 22 | } 23 | 24 | // Tap method performs a touch operation on specified (x,y) coordinate. It 25 | // returns error on adb operation failure. 26 | func (ts TouchScreen) Tap(x int, y int) error { 27 | _, err := ts.dev.Shell("input", "tap", strconv.Itoa(x), strconv.Itoa(y)) 28 | return err 29 | } 30 | 31 | // Swipe method performs touch swipe operation from given (x1, y1) coordinate 32 | // to (x2, y2) coordinate with specified delay. It returns error on adb operation 33 | // failure. 34 | func (ts TouchScreen) Swipe(x1 int, y1 int, x2 int, y2 int, delay int) error { 35 | _, err := ts.dev.Shell("input", "touchscreen", "swipe", strconv.Itoa(x1), strconv.Itoa(y1), strconv.Itoa(x2), strconv.Itoa(y2), strconv.Itoa(delay)) 36 | return err 37 | } 38 | 39 | // SwipeDown method performs touch swipe down (top --> bottom) operation for 40 | // a number of times defined by given count parameter. It returns error on adb operation failure. 41 | func (ts TouchScreen) SwipeDown(count int) error { 42 | w, h, err := ts.disp.GetDisplaySize() 43 | if err != nil { 44 | return err 45 | } 46 | x1 := w / 2 47 | x2 := x1 48 | y1 := h / 4 49 | y2 := y1 * 3 50 | for i := 0; i < count; i++ { 51 | err := ts.Swipe(x1, y1, x2, y2, 1000) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | // SwipeUp method performs touch swipe up (bottom --> top) operation for 60 | // a number of times defined by given count parameter. It returns error on 61 | // adb operation failure. 62 | func (ts TouchScreen) SwipeUp(count int) error { 63 | w, h, err := ts.disp.GetDisplaySize() 64 | if err != nil { 65 | return err 66 | } 67 | x1 := w / 2 68 | x2 := x1 69 | y2 := h / 4 70 | y1 := y2 * 3 71 | for i := 0; i < count; i++ { 72 | err := ts.Swipe(x1, y1, x2, y2, 1000) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | // SwipeLeft method performs touch swipe left (right --> left) operation for 81 | // a number of times defined by given count parameter. It returns error on 82 | // adb operation failure. 83 | func (ts TouchScreen) SwipeLeft(count int) error { 84 | w, h, err := ts.disp.GetDisplaySize() 85 | if err != nil { 86 | return err 87 | } 88 | x2 := w / 4 89 | x1 := x2 * 3 90 | y2 := h / 2 91 | y1 := y2 92 | for i := 0; i < count; i++ { 93 | err := ts.Swipe(x1, y1, x2, y2, 1000) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | // SwipeRight method performs touch swipe right (right --> left) operation for 102 | // a number of times defined by given count parameter. It returns error on 103 | // adb operation failure. 104 | func (ts TouchScreen) SwipeRight(count int) error { 105 | w, h, err := ts.disp.GetDisplaySize() 106 | if err != nil { 107 | return err 108 | } 109 | x1 := w / 4 110 | x2 := x1 * 3 111 | y2 := h / 2 112 | y1 := y2 113 | for i := 0; i < count; i++ { 114 | err := ts.Swipe(x1, y1, x2, y2, 1000) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | // RawSendEvent sends raw touch input event on given touch device, it takes 123 | // event type, event code and event value as parameter and returns error on 124 | // adb operation failure. make sure you are using correct device path for 125 | // touch device, and can be obtailed easily by GetTouchInputDevice method. 126 | func (ts TouchScreen) RawSendEvent(dev string, eventType int, event int, value int) error { 127 | _, err := ts.dev.Shell("sendevent", dev, strconv.Itoa(eventType), strconv.Itoa(event), strconv.Itoa(value)) 128 | return err 129 | } 130 | 131 | // GetTouchInputDevice method is used to determine correct touch input device 132 | // path on associated android device. It returns error on adb operation failure or 133 | // if device path can not be determined for any reason. 134 | func (ts TouchScreen) GetTouchInputDevice() (string, error) { 135 | tag1 := "KEY (0001):" 136 | tag2 := "ABS (0003):" 137 | out, err := ts.dev.Shell("getevent", "-p") 138 | if err != nil { 139 | return "", err 140 | } 141 | lines := strings.Split(out, "\n") 142 | 143 | currentDevice := "" 144 | tag1_match := false 145 | tag2_match := false 146 | for _, line := range lines { 147 | 148 | if strings.Contains(line, "add device") { 149 | tag1_match = false 150 | tag2_match = false 151 | parts := strings.Split(line, ":") 152 | if len(parts) != 2 { 153 | return "", errors.New("Unable to parse device information") 154 | } 155 | currentDevice = strings.TrimSpace(parts[1]) 156 | continue 157 | } 158 | 159 | if strings.Contains(line, tag1) { 160 | tag1_match = true 161 | continue 162 | } 163 | 164 | if strings.Contains(line, tag2) { 165 | tag2_match = true 166 | continue 167 | } 168 | 169 | if tag1_match && tag2_match { 170 | return currentDevice, nil 171 | } 172 | } 173 | return "", errors.New("Unable to determine touch device") 174 | } 175 | -------------------------------------------------------------------------------- /view/descriptionoperations.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func (devView DeviceView) IsDescriptionPresent(description string, index int, timeout int) error { 10 | start := time.Now() 11 | for { 12 | current := time.Now() 13 | delta := current.Sub(start) 14 | if delta.Seconds() >= float64(timeout) { 15 | break 16 | } 17 | vws, err := devView.GetViewes() 18 | if err != nil { 19 | return err 20 | } 21 | _, found := vws.GetByDescription(description, index) 22 | if found { 23 | return nil 24 | } 25 | } 26 | return errors.New(fmt.Sprintf("Timeout occured after [%d] seconds while searching for description [%s]", timeout, description)) 27 | } 28 | 29 | func (devView DeviceView) IsMatchingDescriptionPresnt(description string, index int, timeout int) error { 30 | start := time.Now() 31 | for { 32 | current := time.Now() 33 | delta := current.Sub(start) 34 | if delta.Seconds() >= float64(timeout) { 35 | break 36 | } 37 | vws, err := devView.GetViewes() 38 | if err != nil { 39 | return err 40 | } 41 | _, found := vws.GetByMatchingDescription(description, index) 42 | if found { 43 | return nil 44 | } 45 | } 46 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 47 | } 48 | 49 | func (devView DeviceView) ClickDescription(description string, index int, timeout int) error { 50 | start := time.Now() 51 | for { 52 | current := time.Now() 53 | delta := current.Sub(start) 54 | if delta.Seconds() >= float64(timeout) { 55 | break 56 | } 57 | vws, err := devView.GetViewes() 58 | if err != nil { 59 | return err 60 | } 61 | vw, found := vws.GetByDescription(description, index) 62 | if found { 63 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 64 | } 65 | } 66 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for description [%s]", timeout, description)) 67 | } 68 | 69 | func (devView DeviceView) ClickMatchingDescription(description string, index int, timeout int) error { 70 | start := time.Now() 71 | for { 72 | current := time.Now() 73 | delta := current.Sub(start) 74 | if delta.Seconds() >= float64(timeout) { 75 | break 76 | } 77 | vws, err := devView.GetViewes() 78 | if err != nil { 79 | return err 80 | } 81 | vw, found := vws.GetByMatchingDescription(description, index) 82 | if found { 83 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 84 | } 85 | } 86 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 87 | } 88 | 89 | func (devView DeviceView) GetViewForDescription(description string, index int, timeout int) (View, error) { 90 | start := time.Now() 91 | for { 92 | current := time.Now() 93 | delta := current.Sub(start) 94 | if delta.Seconds() >= float64(timeout) { 95 | break 96 | } 97 | vws, err := devView.GetViewes() 98 | if err != nil { 99 | return View{}, err 100 | } 101 | vw, found := vws.GetByDescription(description, index) 102 | if found { 103 | return vw, nil 104 | } 105 | } 106 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for description [%s]", timeout, description)) 107 | } 108 | 109 | func (devView DeviceView) GetViewForMatchingDescription(description string, index int, timeout int) (View, error) { 110 | start := time.Now() 111 | for { 112 | current := time.Now() 113 | delta := current.Sub(start) 114 | if delta.Seconds() >= float64(timeout) { 115 | break 116 | } 117 | vws, err := devView.GetViewes() 118 | if err != nil { 119 | return View{}, err 120 | } 121 | vw, found := vws.GetByMatchingDescription(description, index) 122 | if found { 123 | return vw, nil 124 | } 125 | } 126 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 127 | } 128 | 129 | func (devView DeviceView) ScrollDownToDescription(description string, index int, maxscroll int) error { 130 | for i := 0; i < maxscroll; i++ { 131 | err := devView.IsDescriptionPresent(description, index, 1) 132 | if err == nil { 133 | return nil 134 | } 135 | devView.im.TouchScreen.SwipeUp(1) 136 | } 137 | return errors.New(fmt.Sprintf("Description [$s] not found after scrolling down [%d] times ", description, maxscroll)) 138 | } 139 | 140 | func (devView DeviceView) ScrollUpToDescription(description string, index int, maxscroll int) error { 141 | for i := 0; i < maxscroll; i++ { 142 | err := devView.IsDescriptionPresent(description, index, 1) 143 | if err == nil { 144 | return nil 145 | } 146 | devView.im.TouchScreen.SwipeDown(1) 147 | } 148 | return errors.New(fmt.Sprintf("Description [$s] not found after scrolling up [%d] times ", description, maxscroll)) 149 | } 150 | 151 | func (devView DeviceView) ScrollDownToMatchingDescription(description string, index int, maxscroll int) error { 152 | for i := 0; i < maxscroll; i++ { 153 | err := devView.IsMatchingDescriptionPresnt(description, index, 1) 154 | if err == nil { 155 | return nil 156 | } 157 | devView.im.TouchScreen.SwipeUp(1) 158 | } 159 | return errors.New(fmt.Sprintf("Matching description [$s] not found after scrolling down [%d] times ", description, maxscroll)) 160 | } 161 | 162 | func (devView DeviceView) ScrollUpToMatchingDescription(description string, index int, maxscroll int) error { 163 | for i := 0; i < maxscroll; i++ { 164 | err := devView.IsMatchingDescriptionPresnt(description, index, 1) 165 | if err == nil { 166 | return nil 167 | } 168 | devView.im.TouchScreen.SwipeDown(1) 169 | } 170 | return errors.New(fmt.Sprintf("Matching description [$s] not found after scrolling up [%d] times ", description, maxscroll)) 171 | } 172 | 173 | func (devView DeviceView) GetDescriptionForText(text string, index int, timeout int) (string, error) { 174 | start := time.Now() 175 | for { 176 | current := time.Now() 177 | delta := current.Sub(start) 178 | if delta.Seconds() >= float64(timeout) { 179 | break 180 | } 181 | vws, err := devView.GetViewes() 182 | if err != nil { 183 | return "", err 184 | } 185 | vw, found := vws.GetByText(text, index) 186 | if found { 187 | return vw.Description, nil 188 | } 189 | } 190 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for text [%s]", timeout, text)) 191 | } 192 | 193 | func (devView DeviceView) GetDescriptionForMatchingText(text string, index int, timeout int) (string, error) { 194 | start := time.Now() 195 | for { 196 | current := time.Now() 197 | delta := current.Sub(start) 198 | if delta.Seconds() >= float64(timeout) { 199 | break 200 | } 201 | vws, err := devView.GetViewes() 202 | if err != nil { 203 | return "", err 204 | } 205 | vw, found := vws.GetByMatchingText(text, index) 206 | if found { 207 | return vw.Description, nil 208 | } 209 | } 210 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matcnhing text [%s]", timeout, text)) 211 | } 212 | 213 | func (devView DeviceView) GetDescriptionForResource(resource string, index int, timeout int) (string, error) { 214 | start := time.Now() 215 | for { 216 | current := time.Now() 217 | delta := current.Sub(start) 218 | if delta.Seconds() >= float64(timeout) { 219 | break 220 | } 221 | vws, err := devView.GetViewes() 222 | if err != nil { 223 | return "", err 224 | } 225 | vw, found := vws.GetByResource(resource, index) 226 | if found { 227 | return vw.Description, nil 228 | } 229 | } 230 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for resource [%s]", timeout, resource)) 231 | } 232 | 233 | func (devView DeviceView) GetDescriptionForMatchingResource(resource string, index int, timeout int) (string, error) { 234 | start := time.Now() 235 | for { 236 | current := time.Now() 237 | delta := current.Sub(start) 238 | if delta.Seconds() >= float64(timeout) { 239 | break 240 | } 241 | vws, err := devView.GetViewes() 242 | if err != nil { 243 | return "", err 244 | } 245 | vw, found := vws.GetByMatchingResource(resource, index) 246 | if found { 247 | return vw.Description, nil 248 | } 249 | } 250 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching resource [%s]", timeout, resource)) 251 | } 252 | 253 | func (devView DeviceView) GetDescriptionFortype(typename string, index int, timeout int) (string, error) { 254 | start := time.Now() 255 | for { 256 | current := time.Now() 257 | delta := current.Sub(start) 258 | if delta.Seconds() >= float64(timeout) { 259 | break 260 | } 261 | vws, err := devView.GetViewes() 262 | if err != nil { 263 | return "", err 264 | } 265 | vw, found := vws.GetByType(typename, index) 266 | if found { 267 | return vw.Description, nil 268 | } 269 | } 270 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for type [%s]", timeout, typename)) 271 | } 272 | 273 | func (devView DeviceView) GetDescriptionForMatchingType(typename string, index int, timeout int) (string, error) { 274 | start := time.Now() 275 | for { 276 | current := time.Now() 277 | delta := current.Sub(start) 278 | if delta.Seconds() >= float64(timeout) { 279 | break 280 | } 281 | vws, err := devView.GetViewes() 282 | if err != nil { 283 | return "", err 284 | } 285 | vw, found := vws.GetByMatchingType(typename, index) 286 | if found { 287 | return vw.Class, nil 288 | } 289 | } 290 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 291 | } 292 | -------------------------------------------------------------------------------- /view/deviceview.go: -------------------------------------------------------------------------------- 1 | // TODO : Documentation 2 | 3 | package view 4 | 5 | import ( 6 | "encoding/xml" 7 | "errors" 8 | "fmt" 9 | "github.com/kunaldawn/goandroid/device" 10 | "github.com/kunaldawn/goandroid/display" 11 | "github.com/kunaldawn/goandroid/input" 12 | "strings" 13 | ) 14 | 15 | type DeviceView struct { 16 | dev device.Device 17 | im input.InputManager 18 | disp display.Display 19 | } 20 | 21 | func NewDeviceView(dev device.Device) DeviceView { 22 | im := input.NewInputManager(dev) 23 | return DeviceView{dev: dev, im: im} 24 | } 25 | 26 | func (devView DeviceView) GetViewes() (Views, error) { 27 | hierarchy, err := devView.GetHierarchy() 28 | if err != nil { 29 | return Views{}, err 30 | } 31 | return hierarchy.ConvertToViews() 32 | } 33 | 34 | func (devView DeviceView) GetHierarchy() (Hierarchy, error) { 35 | out, err := devView.dev.Shell("uiautomator dump") 36 | if err != nil { 37 | return Hierarchy{}, err 38 | } 39 | 40 | var tag = "UI hierchary dumped to:" 41 | if !strings.Contains(out, tag) { 42 | return Hierarchy{}, errors.New(fmt.Sprintf("Unable to locate [%s] in output : %s", tag, out)) 43 | } 44 | parts := strings.Split(out, ":") 45 | if len(parts) != 2 { 46 | return Hierarchy{}, errors.New(fmt.Sprintf("Unable to locate file location in output : %s", out)) 47 | } 48 | xml_location := parts[1] 49 | xml_location = strings.TrimSpace(xml_location) 50 | 51 | xml_data, err := devView.dev.Shell("cat", xml_location) 52 | if err != nil { 53 | return Hierarchy{}, err 54 | } 55 | xml_data = strings.TrimSpace(xml_data) 56 | xml_hierarchy := new(Hierarchy) 57 | err = xml.Unmarshal([]byte(xml_data), xml_hierarchy) 58 | if err != nil { 59 | return Hierarchy{}, err 60 | } 61 | 62 | return *xml_hierarchy, nil 63 | } 64 | -------------------------------------------------------------------------------- /view/hierarchy.go: -------------------------------------------------------------------------------- 1 | // TODO : Documentation 2 | 3 | package view 4 | 5 | import ( 6 | "encoding/xml" 7 | "errors" 8 | "fmt" 9 | "github.com/kunaldawn/goandroid/geometry" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type Hierarchy struct { 15 | XMLName xml.Name `xml:"hierarchy"` // Namespace of the hierarchy 16 | Rotation string `xml:"rotation,attr"` // Rotation value of the hierarchy 17 | NodeList Nodes `xml:"node"` // Child nodes in the hierarchy 18 | } 19 | 20 | type Node struct { 21 | XMLName xml.Name `xml:"node"` // Namespace of the node 22 | Index string `xml:"index,attr"` // Index of the node 23 | Resource string `xml:"resource-id,attr"` // Index of the node 24 | Class string `xml:"class,attr"` // Class of the node 25 | Package string `xml:"package,attr"` // Package of the node 26 | Text string `xml:"text,attr"` // Text of the node 27 | Description string `xml:"content-desc,attr"` // Description of the node 28 | Checkable string `xml:"checkable,attr"` // Checkable status of the node 29 | Checked string `xml:"checked,attr"` // Checked status of the node 30 | Clickable string `xml:"clickable,attr"` // Clickble status of the node 31 | Enabled string `xml:"enabled,attr"` // Enabled status of the node 32 | Focusable string `xml:"focusable,attr"` // Focusable status of the node 33 | Focused string `xml:"focused,attr"` // Focused status of the node 34 | Scrollable string `xml:"scrollable,attr"` // Scrollable status of the node 35 | LongClickable string `xml:"long-clickable,attr"` // Long clickable status of the node 36 | Password string `xml:"password,attr"` // Password field status of the node 37 | Selected string `xml:"selected,attr"` // Selection status of the node 38 | Bounds string `xml:"bounds,attr"` // View bounds of the node 39 | ChildNodes Nodes `xml:"node"` // Child nodes for this node 40 | } 41 | 42 | type Nodes []Node 43 | 44 | func (hierarchy Hierarchy) ConvertToViews() (Views, error) { 45 | return hierarchy.NodeList.ConvertToViews() 46 | } 47 | 48 | func (nodes Nodes) ConvertToViews() (Views, error) { 49 | views := Views{} 50 | for index := range nodes { 51 | v, err := nodes[index].ConvertToView() 52 | if err != nil { 53 | return Views{}, err 54 | } 55 | vv, err := nodes[index].ChildNodes.ConvertToViews() 56 | if err != nil { 57 | return Views{}, err 58 | } 59 | views = append(views, v) 60 | views = append(views, vv...) 61 | } 62 | return views, nil 63 | } 64 | 65 | func (node Node) ConvertToView() (View, error) { 66 | vw := View{} 67 | 68 | index, err := strconv.Atoi(node.Index) 69 | vw.Index = index 70 | if err != nil { 71 | return View{}, err 72 | } 73 | 74 | vw.Class = node.Class 75 | vw.Package = node.Package 76 | 77 | if strings.Contains(node.Resource, ":id/") { 78 | parts := strings.Split(node.Resource, ":id/") 79 | if len(parts) == 2 { 80 | vw.Resource = parts[1] 81 | } else { 82 | vw.Resource = node.Resource 83 | } 84 | } else { 85 | vw.Resource = node.Resource 86 | } 87 | 88 | vw.Text = node.Text 89 | vw.Description = node.Description 90 | 91 | vw.Clickable, _ = strconv.ParseBool(node.Clickable) 92 | vw.Checkable, _ = strconv.ParseBool(node.Checkable) 93 | vw.Checked, _ = strconv.ParseBool(node.Checked) 94 | vw.Enabled, _ = strconv.ParseBool(node.Enabled) 95 | vw.Focusable, _ = strconv.ParseBool(node.Focusable) 96 | vw.Focused, _ = strconv.ParseBool(node.Focused) 97 | vw.Scrollable, _ = strconv.ParseBool(node.Scrollable) 98 | vw.LongClickable, _ = strconv.ParseBool(node.LongClickable) 99 | vw.Password, _ = strconv.ParseBool(node.Password) 100 | vw.Selected, _ = strconv.ParseBool(node.Selected) 101 | 102 | bound_data := node.Bounds 103 | if strings.Contains(bound_data, "[") && strings.Contains(bound_data, "]") && strings.Contains(bound_data, ",") { 104 | parts := strings.Split(bound_data, "][") 105 | if len(parts) == 2 { 106 | top_left := parts[0] 107 | top_left = strings.Replace(top_left, "[", "", -1) 108 | 109 | bottom_right := parts[1] 110 | bottom_right = strings.Replace(bottom_right, "]", "", -1) 111 | 112 | top_parts := strings.Split(top_left, ",") 113 | bottom_parts := strings.Split(bottom_right, ",") 114 | 115 | if (len(top_parts) != 2) || (len(bottom_parts) != 2) { 116 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 117 | } 118 | 119 | top_x, err := strconv.Atoi(top_parts[0]) 120 | if err != nil { 121 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 122 | } 123 | 124 | top_y, err := strconv.Atoi(top_parts[1]) 125 | if err != nil { 126 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 127 | } 128 | 129 | bottom_x, err := strconv.Atoi(bottom_parts[0]) 130 | if err != nil { 131 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 132 | } 133 | 134 | bottom_y, err := strconv.Atoi(bottom_parts[1]) 135 | if err != nil { 136 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 137 | } 138 | 139 | vw.Bound = geometry.Rect{TopLeft: geometry.Point{top_x, top_y}, BottomRight: geometry.Point{bottom_x, bottom_y}} 140 | 141 | center_x := top_x + (bottom_x-top_x)/2 142 | center_y := top_y + (bottom_y-top_y)/2 143 | vw.Center = geometry.Point{X: center_x, Y: center_y} 144 | 145 | } else { 146 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 147 | } 148 | } else { 149 | return View{}, errors.New(fmt.Sprintf("Unable to determine bounds in [%v]", node)) 150 | } 151 | 152 | return vw, nil 153 | } 154 | -------------------------------------------------------------------------------- /view/resourceoperations.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func (devView DeviceView) IsResourcePresent(resource string, index int, timeout int) error { 10 | start := time.Now() 11 | for { 12 | current := time.Now() 13 | delta := current.Sub(start) 14 | if delta.Seconds() >= float64(timeout) { 15 | break 16 | } 17 | vws, err := devView.GetViewes() 18 | if err != nil { 19 | return err 20 | } 21 | _, found := vws.GetByResource(resource, index) 22 | if found { 23 | return nil 24 | } 25 | } 26 | return errors.New(fmt.Sprintf("Timeout occured after [%d] seconds while searching for resource [%s]", timeout, resource)) 27 | } 28 | 29 | func (devView DeviceView) IsMatchingResourcePresnt(resource string, index int, timeout int) error { 30 | start := time.Now() 31 | for { 32 | current := time.Now() 33 | delta := current.Sub(start) 34 | if delta.Seconds() >= float64(timeout) { 35 | break 36 | } 37 | vws, err := devView.GetViewes() 38 | if err != nil { 39 | return err 40 | } 41 | _, found := vws.GetByMatchingResource(resource, index) 42 | if found { 43 | return nil 44 | } 45 | } 46 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching resource [%s]", timeout, resource)) 47 | } 48 | 49 | func (devView DeviceView) ClickResource(resource string, index int, timeout int) error { 50 | start := time.Now() 51 | for { 52 | current := time.Now() 53 | delta := current.Sub(start) 54 | if delta.Seconds() >= float64(timeout) { 55 | break 56 | } 57 | vws, err := devView.GetViewes() 58 | if err != nil { 59 | return err 60 | } 61 | vw, found := vws.GetByResource(resource, index) 62 | if found { 63 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 64 | } 65 | } 66 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for resource [%s]", timeout, resource)) 67 | } 68 | 69 | func (devView DeviceView) ClickMatchingResource(resource string, index int, timeout int) error { 70 | start := time.Now() 71 | for { 72 | current := time.Now() 73 | delta := current.Sub(start) 74 | if delta.Seconds() >= float64(timeout) { 75 | break 76 | } 77 | vws, err := devView.GetViewes() 78 | if err != nil { 79 | return err 80 | } 81 | vw, found := vws.GetByMatchingResource(resource, index) 82 | if found { 83 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 84 | } 85 | } 86 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching resource [%s]", timeout, resource)) 87 | } 88 | 89 | func (devView DeviceView) GetViewForResource(resource string, index int, timeout int) (View, error) { 90 | start := time.Now() 91 | for { 92 | current := time.Now() 93 | delta := current.Sub(start) 94 | if delta.Seconds() >= float64(timeout) { 95 | break 96 | } 97 | vws, err := devView.GetViewes() 98 | if err != nil { 99 | return View{}, err 100 | } 101 | vw, found := vws.GetByResource(resource, index) 102 | if found { 103 | return vw, nil 104 | } 105 | } 106 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for resource [%s]", timeout, resource)) 107 | } 108 | 109 | func (devView DeviceView) GetViewForMatchingResource(resource string, index int, timeout int) (View, error) { 110 | start := time.Now() 111 | for { 112 | current := time.Now() 113 | delta := current.Sub(start) 114 | if delta.Seconds() >= float64(timeout) { 115 | break 116 | } 117 | vws, err := devView.GetViewes() 118 | if err != nil { 119 | return View{}, err 120 | } 121 | vw, found := vws.GetByMatchingResource(resource, index) 122 | if found { 123 | return vw, nil 124 | } 125 | } 126 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching resource [%s]", timeout, resource)) 127 | } 128 | 129 | func (devView DeviceView) ScrollDownToResource(resource string, index int, maxscroll int) error { 130 | for i := 0; i < maxscroll; i++ { 131 | err := devView.IsResourcePresent(resource, index, 1) 132 | if err == nil { 133 | return nil 134 | } 135 | devView.im.TouchScreen.SwipeUp(1) 136 | } 137 | return errors.New(fmt.Sprintf("Resource [$s] not found after scrolling down [%d] times ", resource, maxscroll)) 138 | } 139 | 140 | func (devView DeviceView) ScrollUpToResource(resource string, index int, maxscroll int) error { 141 | for i := 0; i < maxscroll; i++ { 142 | err := devView.IsResourcePresent(resource, index, 1) 143 | if err == nil { 144 | return nil 145 | } 146 | devView.im.TouchScreen.SwipeDown(1) 147 | } 148 | return errors.New(fmt.Sprintf("Resource [$s] not found after scrolling up [%d] times ", resource, maxscroll)) 149 | } 150 | 151 | func (devView DeviceView) ScrollDownToMatchingResource(resource string, index int, maxscroll int) error { 152 | for i := 0; i < maxscroll; i++ { 153 | err := devView.IsMatchingTextPresnt(resource, index, 1) 154 | if err == nil { 155 | return nil 156 | } 157 | devView.im.TouchScreen.SwipeUp(1) 158 | } 159 | return errors.New(fmt.Sprintf("Matching resource [$s] not found after scrolling down [%d] times ", resource, maxscroll)) 160 | } 161 | 162 | func (devView DeviceView) ScrollUpToMatchingResource(resource string, index int, maxscroll int) error { 163 | for i := 0; i < maxscroll; i++ { 164 | err := devView.IsMatchingTextPresnt(resource, index, 1) 165 | if err == nil { 166 | return nil 167 | } 168 | devView.im.TouchScreen.SwipeDown(1) 169 | } 170 | return errors.New(fmt.Sprintf("Matching resource [$s] not found after scrolling up [%d] times ", resource, maxscroll)) 171 | } 172 | 173 | func (devView DeviceView) GetResourceForText(text string, index int, timeout int) (string, error) { 174 | start := time.Now() 175 | for { 176 | current := time.Now() 177 | delta := current.Sub(start) 178 | if delta.Seconds() >= float64(timeout) { 179 | break 180 | } 181 | vws, err := devView.GetViewes() 182 | if err != nil { 183 | return "", err 184 | } 185 | vw, found := vws.GetByText(text, index) 186 | if found { 187 | return vw.Resource, nil 188 | } 189 | } 190 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for text [%s]", timeout, text)) 191 | } 192 | 193 | func (devView DeviceView) GetResourceForMatchingText(text string, index int, timeout int) (string, error) { 194 | start := time.Now() 195 | for { 196 | current := time.Now() 197 | delta := current.Sub(start) 198 | if delta.Seconds() >= float64(timeout) { 199 | break 200 | } 201 | vws, err := devView.GetViewes() 202 | if err != nil { 203 | return "", err 204 | } 205 | vw, found := vws.GetByMatchingText(text, index) 206 | if found { 207 | return vw.Resource, nil 208 | } 209 | } 210 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matcnhing text [%s]", timeout, text)) 211 | } 212 | 213 | func (devView DeviceView) GetResourceForType(typename string, index int, timeout int) (string, error) { 214 | start := time.Now() 215 | for { 216 | current := time.Now() 217 | delta := current.Sub(start) 218 | if delta.Seconds() >= float64(timeout) { 219 | break 220 | } 221 | vws, err := devView.GetViewes() 222 | if err != nil { 223 | return "", err 224 | } 225 | vw, found := vws.GetByType(typename, index) 226 | if found { 227 | return vw.Resource, nil 228 | } 229 | } 230 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for type [%s]", timeout, typename)) 231 | } 232 | 233 | func (devView DeviceView) GetResourceForMatchingType(typename string, index int, timeout int) (string, error) { 234 | start := time.Now() 235 | for { 236 | current := time.Now() 237 | delta := current.Sub(start) 238 | if delta.Seconds() >= float64(timeout) { 239 | break 240 | } 241 | vws, err := devView.GetViewes() 242 | if err != nil { 243 | return "", err 244 | } 245 | vw, found := vws.GetByMatchingType(typename, index) 246 | if found { 247 | return vw.Resource, nil 248 | } 249 | } 250 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 251 | } 252 | 253 | func (devView DeviceView) GetResourceForDescription(description string, index int, timeout int) (string, error) { 254 | start := time.Now() 255 | for { 256 | current := time.Now() 257 | delta := current.Sub(start) 258 | if delta.Seconds() >= float64(timeout) { 259 | break 260 | } 261 | vws, err := devView.GetViewes() 262 | if err != nil { 263 | return "", err 264 | } 265 | vw, found := vws.GetByDescription(description, index) 266 | if found { 267 | return vw.Resource, nil 268 | } 269 | } 270 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for description [%s]", timeout, description)) 271 | } 272 | 273 | func (devView DeviceView) GetResourceForMatchingDescription(description string, index int, timeout int) (string, error) { 274 | start := time.Now() 275 | for { 276 | current := time.Now() 277 | delta := current.Sub(start) 278 | if delta.Seconds() >= float64(timeout) { 279 | break 280 | } 281 | vws, err := devView.GetViewes() 282 | if err != nil { 283 | return "", err 284 | } 285 | vw, found := vws.GetByMatchingDescription(description, index) 286 | if found { 287 | return vw.Resource, nil 288 | } 289 | } 290 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 291 | } 292 | -------------------------------------------------------------------------------- /view/textoperations.go: -------------------------------------------------------------------------------- 1 | // TODO : Documentation 2 | 3 | package view 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | func (devView DeviceView) IsTextPresent(text string, index int, timeout int) error { 12 | start := time.Now() 13 | for { 14 | current := time.Now() 15 | delta := current.Sub(start) 16 | if delta.Seconds() >= float64(timeout) { 17 | break 18 | } 19 | vws, err := devView.GetViewes() 20 | if err != nil { 21 | return err 22 | } 23 | _, found := vws.GetByText(text, index) 24 | if found { 25 | return nil 26 | } 27 | } 28 | return errors.New(fmt.Sprintf("Timeout occured after [%d] seconds while searching for text [%s]", timeout, text)) 29 | } 30 | 31 | func (devView DeviceView) IsMatchingTextPresnt(text string, index int, timeout int) error { 32 | start := time.Now() 33 | for { 34 | current := time.Now() 35 | delta := current.Sub(start) 36 | if delta.Seconds() >= float64(timeout) { 37 | break 38 | } 39 | vws, err := devView.GetViewes() 40 | if err != nil { 41 | return err 42 | } 43 | _, found := vws.GetByMatchingText(text, index) 44 | if found { 45 | return nil 46 | } 47 | } 48 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching text [%s]", timeout, text)) 49 | } 50 | 51 | func (devView DeviceView) ClickText(text string, index int, timeout int) error { 52 | start := time.Now() 53 | for { 54 | current := time.Now() 55 | delta := current.Sub(start) 56 | if delta.Seconds() >= float64(timeout) { 57 | break 58 | } 59 | vws, err := devView.GetViewes() 60 | if err != nil { 61 | return err 62 | } 63 | vw, found := vws.GetByText(text, index) 64 | if found { 65 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 66 | } 67 | } 68 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for text [%s]", timeout, text)) 69 | } 70 | 71 | func (devView DeviceView) ClickMatchingText(text string, index int, timeout int) error { 72 | start := time.Now() 73 | for { 74 | current := time.Now() 75 | delta := current.Sub(start) 76 | if delta.Seconds() >= float64(timeout) { 77 | break 78 | } 79 | vws, err := devView.GetViewes() 80 | if err != nil { 81 | return err 82 | } 83 | vw, found := vws.GetByMatchingText(text, index) 84 | if found { 85 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 86 | } 87 | } 88 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching text [%s]", timeout, text)) 89 | } 90 | 91 | func (devView DeviceView) GetViewForText(text string, index int, timeout int) (View, error) { 92 | start := time.Now() 93 | for { 94 | current := time.Now() 95 | delta := current.Sub(start) 96 | if delta.Seconds() >= float64(timeout) { 97 | break 98 | } 99 | vws, err := devView.GetViewes() 100 | if err != nil { 101 | return View{}, err 102 | } 103 | vw, found := vws.GetByText(text, index) 104 | if found { 105 | return vw, nil 106 | } 107 | } 108 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for text [%s]", timeout, text)) 109 | } 110 | 111 | func (devView DeviceView) GetViewForMatchingText(text string, index int, timeout int) (View, error) { 112 | start := time.Now() 113 | for { 114 | current := time.Now() 115 | delta := current.Sub(start) 116 | if delta.Seconds() >= float64(timeout) { 117 | break 118 | } 119 | vws, err := devView.GetViewes() 120 | if err != nil { 121 | return View{}, err 122 | } 123 | vw, found := vws.GetByMatchingText(text, index) 124 | if found { 125 | return vw, nil 126 | } 127 | } 128 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching text [%s]", timeout, text)) 129 | } 130 | 131 | func (devView DeviceView) ScrollDownToText(text string, index int, maxscroll int) error { 132 | for i := 0; i < maxscroll; i++ { 133 | err := devView.IsTextPresent(text, index, 1) 134 | if err == nil { 135 | return nil 136 | } 137 | devView.im.TouchScreen.SwipeUp(1) 138 | } 139 | return errors.New(fmt.Sprintf("Text [%s] not found after scrolling down [%d] times ", text, maxscroll)) 140 | } 141 | 142 | func (devView DeviceView) ScrollUpToText(text string, index int, maxscroll int) error { 143 | for i := 0; i < maxscroll; i++ { 144 | err := devView.IsTextPresent(text, index, 1) 145 | if err == nil { 146 | return nil 147 | } 148 | devView.im.TouchScreen.SwipeDown(1) 149 | } 150 | return errors.New(fmt.Sprintf("Text [%s] not found after scrolling up [%d] times ", text, maxscroll)) 151 | } 152 | 153 | func (devView DeviceView) ScrollDownToMatchingText(text string, index int, maxscroll int) error { 154 | for i := 0; i < maxscroll; i++ { 155 | err := devView.IsMatchingTextPresnt(text, index, 1) 156 | if err == nil { 157 | return nil 158 | } 159 | devView.im.TouchScreen.SwipeUp(1) 160 | } 161 | return errors.New(fmt.Sprintf("Matching text [%s] not found after scrolling down [%d] times ", text, maxscroll)) 162 | } 163 | 164 | func (devView DeviceView) ScrollUpToMatchingText(text string, index int, maxscroll int) error { 165 | for i := 0; i < maxscroll; i++ { 166 | err := devView.IsMatchingTextPresnt(text, index, 1) 167 | if err == nil { 168 | return nil 169 | } 170 | devView.im.TouchScreen.SwipeDown(1) 171 | } 172 | return errors.New(fmt.Sprintf("Matching text [%s] not found after scrolling up [%d] times ", text, maxscroll)) 173 | } 174 | 175 | func (devView DeviceView) GetTextForResource(resource string, index int, timeout int) (string, error) { 176 | start := time.Now() 177 | for { 178 | current := time.Now() 179 | delta := current.Sub(start) 180 | if delta.Seconds() >= float64(timeout) { 181 | break 182 | } 183 | vws, err := devView.GetViewes() 184 | if err != nil { 185 | return "", err 186 | } 187 | vw, found := vws.GetByResource(resource, index) 188 | if found { 189 | return vw.Text, nil 190 | } 191 | } 192 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for resource [%s]", timeout, resource)) 193 | } 194 | 195 | func (devView DeviceView) GetTextForMatchingResource(resource string, index int, timeout int) (string, error) { 196 | start := time.Now() 197 | for { 198 | current := time.Now() 199 | delta := current.Sub(start) 200 | if delta.Seconds() >= float64(timeout) { 201 | break 202 | } 203 | vws, err := devView.GetViewes() 204 | if err != nil { 205 | return "", err 206 | } 207 | vw, found := vws.GetByMatchingResource(resource, index) 208 | if found { 209 | return vw.Text, nil 210 | } 211 | } 212 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matcnhing resource [%s]", timeout, resource)) 213 | } 214 | 215 | func (devView DeviceView) GetTextForType(typename string, index int, timeout int) (string, error) { 216 | start := time.Now() 217 | for { 218 | current := time.Now() 219 | delta := current.Sub(start) 220 | if delta.Seconds() >= float64(timeout) { 221 | break 222 | } 223 | vws, err := devView.GetViewes() 224 | if err != nil { 225 | return "", err 226 | } 227 | vw, found := vws.GetByType(typename, index) 228 | if found { 229 | return vw.Text, nil 230 | } 231 | } 232 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for type [%s]", timeout, typename)) 233 | } 234 | 235 | func (devView DeviceView) GetTextForMatchingType(typename string, index int, timeout int) (string, error) { 236 | start := time.Now() 237 | for { 238 | current := time.Now() 239 | delta := current.Sub(start) 240 | if delta.Seconds() >= float64(timeout) { 241 | break 242 | } 243 | vws, err := devView.GetViewes() 244 | if err != nil { 245 | return "", err 246 | } 247 | vw, found := vws.GetByMatchingType(typename, index) 248 | if found { 249 | return vw.Text, nil 250 | } 251 | } 252 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 253 | } 254 | 255 | func (devView DeviceView) GetTextForDescription(description string, index int, timeout int) (string, error) { 256 | start := time.Now() 257 | for { 258 | current := time.Now() 259 | delta := current.Sub(start) 260 | if delta.Seconds() >= float64(timeout) { 261 | break 262 | } 263 | vws, err := devView.GetViewes() 264 | if err != nil { 265 | return "", err 266 | } 267 | vw, found := vws.GetByDescription(description, index) 268 | if found { 269 | return vw.Text, nil 270 | } 271 | } 272 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for description [%s]", timeout, description)) 273 | } 274 | 275 | func (devView DeviceView) GetTextForMatchingDescription(description string, index int, timeout int) (string, error) { 276 | start := time.Now() 277 | for { 278 | current := time.Now() 279 | delta := current.Sub(start) 280 | if delta.Seconds() >= float64(timeout) { 281 | break 282 | } 283 | vws, err := devView.GetViewes() 284 | if err != nil { 285 | return "", err 286 | } 287 | vw, found := vws.GetByMatchingDescription(description, index) 288 | if found { 289 | return vw.Text, nil 290 | } 291 | } 292 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 293 | } 294 | -------------------------------------------------------------------------------- /view/typeoperations.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func (devView DeviceView) IsTypePresent(typename string, index int, timeout int) error { 10 | start := time.Now() 11 | for { 12 | current := time.Now() 13 | delta := current.Sub(start) 14 | if delta.Seconds() >= float64(timeout) { 15 | break 16 | } 17 | vws, err := devView.GetViewes() 18 | if err != nil { 19 | return err 20 | } 21 | _, found := vws.GetByType(typename, index) 22 | if found { 23 | return nil 24 | } 25 | } 26 | return errors.New(fmt.Sprintf("Timeout occured after [%d] seconds while searching for type [%s]", timeout, typename)) 27 | } 28 | 29 | func (devView DeviceView) IsMatchingTypePresnt(typename string, index int, timeout int) error { 30 | start := time.Now() 31 | for { 32 | current := time.Now() 33 | delta := current.Sub(start) 34 | if delta.Seconds() >= float64(timeout) { 35 | break 36 | } 37 | vws, err := devView.GetViewes() 38 | if err != nil { 39 | return err 40 | } 41 | _, found := vws.GetByMatchingType(typename, index) 42 | if found { 43 | return nil 44 | } 45 | } 46 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 47 | } 48 | 49 | func (devView DeviceView) ClickType(typename string, index int, timeout int) error { 50 | start := time.Now() 51 | for { 52 | current := time.Now() 53 | delta := current.Sub(start) 54 | if delta.Seconds() >= float64(timeout) { 55 | break 56 | } 57 | vws, err := devView.GetViewes() 58 | if err != nil { 59 | return err 60 | } 61 | vw, found := vws.GetByType(typename, index) 62 | if found { 63 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 64 | } 65 | } 66 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for type [%s]", timeout, typename)) 67 | } 68 | 69 | func (devView DeviceView) ClickMatchingType(typename string, index int, timeout int) error { 70 | start := time.Now() 71 | for { 72 | current := time.Now() 73 | delta := current.Sub(start) 74 | if delta.Seconds() >= float64(timeout) { 75 | break 76 | } 77 | vws, err := devView.GetViewes() 78 | if err != nil { 79 | return err 80 | } 81 | vw, found := vws.GetByMatchingType(typename, index) 82 | if found { 83 | return devView.im.TouchScreen.Tap(vw.Center.X, vw.Center.Y) 84 | } 85 | } 86 | return errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 87 | } 88 | 89 | func (devView DeviceView) GetViewForType(typename string, index int, timeout int) (View, error) { 90 | start := time.Now() 91 | for { 92 | current := time.Now() 93 | delta := current.Sub(start) 94 | if delta.Seconds() >= float64(timeout) { 95 | break 96 | } 97 | vws, err := devView.GetViewes() 98 | if err != nil { 99 | return View{}, err 100 | } 101 | vw, found := vws.GetByType(typename, index) 102 | if found { 103 | return vw, nil 104 | } 105 | } 106 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for type [%s]", timeout, typename)) 107 | } 108 | 109 | func (devView DeviceView) GetViewForMatchingType(typename string, index int, timeout int) (View, error) { 110 | start := time.Now() 111 | for { 112 | current := time.Now() 113 | delta := current.Sub(start) 114 | if delta.Seconds() >= float64(timeout) { 115 | break 116 | } 117 | vws, err := devView.GetViewes() 118 | if err != nil { 119 | return View{}, err 120 | } 121 | vw, found := vws.GetByMatchingType(typename, index) 122 | if found { 123 | return vw, nil 124 | } 125 | } 126 | return View{}, errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching type [%s]", timeout, typename)) 127 | } 128 | 129 | func (devView DeviceView) ScrollDownToType(typename string, index int, maxscroll int) error { 130 | for i := 0; i < maxscroll; i++ { 131 | err := devView.IsTypePresent(typename, index, 1) 132 | if err == nil { 133 | return nil 134 | } 135 | devView.im.TouchScreen.SwipeUp(1) 136 | } 137 | return errors.New(fmt.Sprintf("Type [$s] not found after scrolling down [%d] times ", typename, maxscroll)) 138 | } 139 | 140 | func (devView DeviceView) ScrollUpToType(typename string, index int, maxscroll int) error { 141 | for i := 0; i < maxscroll; i++ { 142 | err := devView.IsTypePresent(typename, index, 1) 143 | if err == nil { 144 | return nil 145 | } 146 | devView.im.TouchScreen.SwipeDown(1) 147 | } 148 | return errors.New(fmt.Sprintf("Type [$s] not found after scrolling up [%d] times ", typename, maxscroll)) 149 | } 150 | 151 | func (devView DeviceView) ScrollDownToMatchingType(typename string, index int, maxscroll int) error { 152 | for i := 0; i < maxscroll; i++ { 153 | err := devView.IsMatchingTypePresnt(typename, index, 1) 154 | if err == nil { 155 | return nil 156 | } 157 | devView.im.TouchScreen.SwipeUp(1) 158 | } 159 | return errors.New(fmt.Sprintf("Matching type [$s] not found after scrolling down [%d] times ", typename, maxscroll)) 160 | } 161 | 162 | func (devView DeviceView) ScrollUpToMatchingType(typename string, index int, maxscroll int) error { 163 | for i := 0; i < maxscroll; i++ { 164 | err := devView.IsMatchingTypePresnt(typename, index, 1) 165 | if err == nil { 166 | return nil 167 | } 168 | devView.im.TouchScreen.SwipeDown(1) 169 | } 170 | return errors.New(fmt.Sprintf("Matching type [$s] not found after scrolling up [%d] times ", typename, maxscroll)) 171 | } 172 | 173 | func (devView DeviceView) GetTypeForText(text string, index int, timeout int) (string, error) { 174 | start := time.Now() 175 | for { 176 | current := time.Now() 177 | delta := current.Sub(start) 178 | if delta.Seconds() >= float64(timeout) { 179 | break 180 | } 181 | vws, err := devView.GetViewes() 182 | if err != nil { 183 | return "", err 184 | } 185 | vw, found := vws.GetByText(text, index) 186 | if found { 187 | return vw.Class, nil 188 | } 189 | } 190 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for text [%s]", timeout, text)) 191 | } 192 | 193 | func (devView DeviceView) GetTypeForMatchingText(text string, index int, timeout int) (string, error) { 194 | start := time.Now() 195 | for { 196 | current := time.Now() 197 | delta := current.Sub(start) 198 | if delta.Seconds() >= float64(timeout) { 199 | break 200 | } 201 | vws, err := devView.GetViewes() 202 | if err != nil { 203 | return "", err 204 | } 205 | vw, found := vws.GetByMatchingText(text, index) 206 | if found { 207 | return vw.Class, nil 208 | } 209 | } 210 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matcnhing text [%s]", timeout, text)) 211 | } 212 | 213 | func (devView DeviceView) GetTypeForResource(resource string, index int, timeout int) (string, error) { 214 | start := time.Now() 215 | for { 216 | current := time.Now() 217 | delta := current.Sub(start) 218 | if delta.Seconds() >= float64(timeout) { 219 | break 220 | } 221 | vws, err := devView.GetViewes() 222 | if err != nil { 223 | return "", err 224 | } 225 | vw, found := vws.GetByResource(resource, index) 226 | if found { 227 | return vw.Class, nil 228 | } 229 | } 230 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for resource [%s]", timeout, resource)) 231 | } 232 | 233 | func (devView DeviceView) GetTypeForMatchingResource(resource string, index int, timeout int) (string, error) { 234 | start := time.Now() 235 | for { 236 | current := time.Now() 237 | delta := current.Sub(start) 238 | if delta.Seconds() >= float64(timeout) { 239 | break 240 | } 241 | vws, err := devView.GetViewes() 242 | if err != nil { 243 | return "", err 244 | } 245 | vw, found := vws.GetByMatchingResource(resource, index) 246 | if found { 247 | return vw.Class, nil 248 | } 249 | } 250 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching resource [%s]", timeout, resource)) 251 | } 252 | 253 | func (devView DeviceView) GetTypeForDescription(description string, index int, timeout int) (string, error) { 254 | start := time.Now() 255 | for { 256 | current := time.Now() 257 | delta := current.Sub(start) 258 | if delta.Seconds() >= float64(timeout) { 259 | break 260 | } 261 | vws, err := devView.GetViewes() 262 | if err != nil { 263 | return "", err 264 | } 265 | vw, found := vws.GetByDescription(description, index) 266 | if found { 267 | return vw.Class, nil 268 | } 269 | } 270 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for description [%s]", timeout, description)) 271 | } 272 | 273 | func (devView DeviceView) GetTypeForMatchingDescription(description string, index int, timeout int) (string, error) { 274 | start := time.Now() 275 | for { 276 | current := time.Now() 277 | delta := current.Sub(start) 278 | if delta.Seconds() >= float64(timeout) { 279 | break 280 | } 281 | vws, err := devView.GetViewes() 282 | if err != nil { 283 | return "", err 284 | } 285 | vw, found := vws.GetByMatchingDescription(description, index) 286 | if found { 287 | return vw.Class, nil 288 | } 289 | } 290 | return "", errors.New(fmt.Sprintf("Timeout occured after %d seconds while searching for matching description [%s]", timeout, description)) 291 | } 292 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | // Package view provides various query and UI operation methods on android 2 | // UI View. It internally uses uiautomator XML dump mechanism to parse device 3 | // view hierarchy, so all operations are based on information extracted from 4 | // XML hierarchy dump. 5 | // TODO : Detailed package documentation need to be done here. 6 | package view 7 | 8 | import ( 9 | "github.com/kunaldawn/goandroid/geometry" 10 | "strings" 11 | ) 12 | 13 | // View struct is an internal representation of android ui component. This is 14 | // generated from uiautomaror XML dump but in a representation that is more 15 | // suitable for various UI operations by goandroid framework. It is a typed 16 | // representation of uiautomator node. Some extra fields are calculated for 17 | // ease of use, such as center of the view, which allows to click the view 18 | // and index of the view. Index is calculated based on occurence of the 19 | // node in uiautomator xml dump. 20 | type View struct { 21 | Index int // Index value of the android view component. 22 | Class string // Canonical class name of the android view component. 23 | Package string // Canonical package name of the android view component. 24 | Resource string // Associated resource id (if any) of the view component. 25 | Text string // Associated text (if any) of the view component. 26 | Description string // Associated description (if any) of the view component. 27 | Clickable bool // Boolean value indicating if the view is clickable. 28 | Checkable bool // Boolean value indicating if the view is checkbox or not. 29 | Checked bool // Boolean value indicating if the view is checked or not. 30 | Enabled bool // Boolean value indicating if the view is enabled or not. 31 | Focusable bool // Boolean value indicating if the view is focusable by user or not. 32 | Focused bool // Boolean value indicating if the view is currently focused or not. 33 | Scrollable bool // Boolean value indicating if the view is scrollable or not. 34 | LongClickable bool // Boolean value indicating if the view is long click enabled or not. 35 | Password bool // Boolean value indicating if the view is a password field or not. 36 | Selected bool // Boolean value indicating if the view is currently selected by user or not. 37 | Bound geometry.Rect // Bounding rectangle of the view. 38 | Center geometry.Point // Center coordinate of the view. 39 | } 40 | 41 | // Views is a type that represents sclice of View structure. 42 | type Views []View 43 | 44 | // GetByText method returns a View struct based on excat match of specified 45 | // text and index. It also returns a boolean value indicating if the match 46 | // is found or not. Please note that parameter index is zero based index 47 | // system, that is, index value of first element is zero. If no view is 48 | // found with exact text match, empty view is returned with boolean value 49 | // false indicating view is not found. 50 | func (views Views) GetByText(text string, index int) (View, bool) { 51 | idx := 0 52 | for _, vw := range views { 53 | if vw.Text == text { 54 | if idx == index { 55 | return vw, true 56 | } 57 | idx += 1 58 | } 59 | } 60 | return View{}, false 61 | } 62 | 63 | // GetByMatchingText method returns a View struct on matching text with 64 | // specified index. It also returns a boolean value indicating if the match 65 | // is found or not. Please note that parameter index is zero based index 66 | // system, that is, index value of first element is zero. If no view is 67 | // found with matching text, empty view is returned with boolean value 68 | // false indicating view is not found. Text match is case insensitive. 69 | func (views Views) GetByMatchingText(text string, index int) (View, bool) { 70 | idx := 0 71 | for _, vw := range views { 72 | if strings.Contains(strings.ToLower(vw.Text), strings.ToLower(text)) { 73 | if idx == index { 74 | return vw, true 75 | } 76 | idx += 1 77 | } 78 | } 79 | return View{}, false 80 | } 81 | 82 | // GetByResource method returns a View struct based on excat match of 83 | // specified text value of view resource id and index. It also returns a 84 | // boolean value indicating if the match is found or not. Please note that 85 | // parameter index is zero based index system, that is, index value of first 86 | // element is zero. If no view is found with exact resource id match, empty 87 | // view is returned with boolean value false indicating view is not found. 88 | // NOTE : Here resource id represents following "" part only 89 | // :id/ 90 | func (views Views) GetByResource(resource string, index int) (View, bool) { 91 | idx := 0 92 | for _, vw := range views { 93 | if vw.Resource == resource { 94 | if idx == index { 95 | return vw, true 96 | } 97 | idx += 1 98 | } 99 | } 100 | return View{}, false 101 | } 102 | 103 | // GetByMatchingResource method returns a View struct on matching text value 104 | // of resource id with specified index. It also returns a boolean value 105 | // indicating if the match is found or not. Please note that parameter index 106 | // is zero based index system, that is, index value of first element is zero. 107 | // If no view is found with matching test value of resource id, empty view is 108 | // returned with boolean value false indicating view is not found. Resource 109 | // id match is case insensitive. 110 | // NOTE : Here resource id represents following "" part only 111 | // :id/ 112 | func (views Views) GetByMatchingResource(resource string, index int) (View, bool) { 113 | idx := 0 114 | for _, vw := range views { 115 | if strings.Contains(strings.ToLower(vw.Resource), strings.ToLower(resource)) { 116 | if idx == index { 117 | return vw, true 118 | } 119 | idx += 1 120 | } 121 | } 122 | return View{}, false 123 | } 124 | 125 | // GetByDescription method returns a View struct based on excat match of 126 | // specified text value of views description and index. It also returns a 127 | // boolean value indicating if the match is found or not. Please note that 128 | // parameter index is zero based index system, that is, index value of first 129 | // element is zero. If no view is found with exact description text match, 130 | // empty view is returned with boolean value false indicating view is not found. 131 | func (views Views) GetByDescription(description string, index int) (View, bool) { 132 | idx := 0 133 | for _, vw := range views { 134 | if vw.Description == description { 135 | if idx == index { 136 | return vw, true 137 | } 138 | idx += 1 139 | } 140 | } 141 | return View{}, false 142 | } 143 | 144 | // GetByMatchingDescription method returns a View struct on matching text 145 | // value of description field of the view with specified index. It also 146 | // returns a boolean value indicating if the match is found or not. Please 147 | // note that parameter index is zero based index system, that is, index value 148 | // of first element is zero. If no view is found with matching description 149 | // value, empty view is returned with boolean value false indicating view is 150 | // not found. Description match is case insensitive. 151 | func (views Views) GetByMatchingDescription(description string, index int) (View, bool) { 152 | idx := 0 153 | for _, vw := range views { 154 | if strings.Contains(strings.ToLower(vw.Description), strings.ToLower(description)) { 155 | if idx == index { 156 | return vw, true 157 | } 158 | idx += 1 159 | } 160 | } 161 | return View{}, false 162 | } 163 | 164 | // GetByType method returns a View struct based on excat match of specified 165 | // text value of views class name and index. It also returns a boolean value 166 | // indicating if the match is found or not. Please note that parameter index 167 | // is zero based index system, that is, index value of first element is zero. 168 | // If no view is found with exact class name match, empty view is returned 169 | // with boolean value false indicating view is not found. 170 | func (views Views) GetByType(typename string, index int) (View, bool) { 171 | idx := 0 172 | for _, vw := range views { 173 | if vw.Class == typename { 174 | if idx == index { 175 | return vw, true 176 | } 177 | idx += 1 178 | } 179 | } 180 | return View{}, false 181 | } 182 | 183 | // GetByMatchingType method returns a View struct on matching text value of 184 | // class name field of the view with specified index. It also returns a 185 | // boolean value indicating if the match is found or not. Please note that 186 | // parameter index is zero based index system, that is, index value of first 187 | // element is zero. If no view is found with matching class name, empty view 188 | // is returned with boolean value false indicating view is not found. Class 189 | // name match is case insensitive. 190 | func (views Views) GetByMatchingType(typename string, index int) (View, bool) { 191 | idx := 0 192 | for _, vw := range views { 193 | if strings.Contains(strings.ToLower(vw.Class), strings.ToLower(typename)) { 194 | if idx == index { 195 | return vw, true 196 | } 197 | idx += 1 198 | } 199 | } 200 | return View{}, false 201 | } 202 | --------------------------------------------------------------------------------