├── README ├── eventrecorder ├── README.md └── src │ ├── android │ ├── AndroidManifest.xml │ ├── bin │ │ └── EventRecorder.apk │ ├── build.properties │ ├── build.xml │ ├── default.properties │ ├── res │ │ ├── drawable-mdpi │ │ │ └── icon.png │ │ ├── raw │ │ │ └── template.py │ │ └── values │ │ │ └── strings.xml │ └── src │ │ └── com │ │ └── sencha │ │ └── eventrecorder │ │ ├── App.java │ │ ├── Base64.java │ │ ├── EventWriter.java │ │ ├── FileAccess.java │ │ ├── HandlerTimer.java │ │ ├── PlaybackFileAccess.java │ │ ├── Reply.java │ │ └── Storage.java │ ├── examples │ ├── results │ │ ├── carousel │ │ │ └── droidincredible │ │ │ │ └── froyo │ │ │ │ ├── screen1.png │ │ │ │ └── screen2.png │ │ ├── forms │ │ │ └── nexusone │ │ │ │ ├── froyo │ │ │ │ └── screen1.png │ │ │ │ └── gingerbread │ │ │ │ └── screen1.png │ │ ├── icons │ │ │ └── evo4g │ │ │ │ └── froyo │ │ │ │ ├── screen1.png │ │ │ │ └── screen2.png │ │ ├── overlays │ │ │ ├── screen1.png │ │ │ ├── screen2.png │ │ │ ├── screen3.png │ │ │ └── screen4.png │ │ ├── picker │ │ │ └── nexusone │ │ │ │ └── gingerbread │ │ │ │ ├── console.log │ │ │ │ ├── screen1.png │ │ │ │ ├── screen2.png │ │ │ │ └── screen3.png │ │ └── tabs2 │ │ │ ├── evo4g │ │ │ └── froyo │ │ │ │ ├── screen1.png │ │ │ │ ├── screen2.png │ │ │ │ ├── screen3.png │ │ │ │ ├── screen4.png │ │ │ │ └── screen5.png │ │ │ └── nexusone │ │ │ └── froyo │ │ │ ├── screen1.png │ │ │ ├── screen2.png │ │ │ ├── screen3.png │ │ │ ├── screen4.png │ │ │ └── screen5.png │ └── tests │ │ ├── carousel.py │ │ ├── forms.py │ │ ├── icons.py │ │ ├── overlays.py │ │ ├── picker.py │ │ └── tabs2.py │ └── shell │ ├── imagediff.py │ ├── inline-apk.py │ └── recorder.py └── remotejs ├── README.md └── src ├── android ├── .classpath ├── .project ├── AndroidManifest.xml ├── README.md ├── bin │ └── RemoteJS.apk ├── default.properties ├── generate.ant.sh ├── res │ ├── drawable-mdpi │ │ └── icon.png │ └── values │ │ └── strings.xml └── src │ └── com │ └── sencha │ └── remotejs │ ├── Base64.java │ └── RemoteJS.java ├── desktop ├── README.md ├── codemirror │ ├── LICENSE │ ├── css │ │ └── jscolors.css │ └── js │ │ ├── highlight.js │ │ ├── parsejavascript.js │ │ ├── stringstream.js │ │ ├── tokenize.js │ │ └── tokenizejavascript.js ├── generate.dmg.sh ├── interface.html ├── interface.js ├── remotejs.cpp ├── remotejs.pro ├── remotejs.qrc └── sencha_logo.png └── shell ├── README.md ├── adb.py ├── png.py ├── remotejs.py ├── test_example.py └── update-apk.py /README: -------------------------------------------------------------------------------- 1 | Collection of tools to help web developers on Android 2 | 3 | Copyright (c) 2010 Sencha Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /eventrecorder/README.md: -------------------------------------------------------------------------------- 1 | EventRecorder 2 | === 3 | 4 | What it is 5 | --- 6 | _EventRecorder_ is an infrastructure that provides automated web application testing on Android-based devices. It consists of a device tool and a host machine recording client. This client application receives a playback script from the device tool at the end of the recording. The script is a self-contained program that is able to contact the device tool and send all the recorded events in order to reproduce the session as closely as possible. 7 | 8 | Currently it is possible to: 9 | 10 | * Load URLs 11 | * Execute JavaScript and log evaluations via _console.log()_ 12 | * Record and play back touch and physical keyboard events 13 | * Capture the device screen leaving status and title bars out 14 | * Fill text in form fields via the host machine 15 | 16 | Supported platforms 17 | --- 18 | Any platform that is simultaneously supported by the [Android Debug Bridge](http://developer.android.com/guide/developing/tools/adb.html) (adb), and [Python](http://www.python.org/). 19 | Currently, it is targeted at devices running [Android](http://www.android.com) and it has been tested with Froyo, Gingerbread and Honeycomb. If you successfully use it on Eclair, please let us know. You can either use a real device or an [emulator](http://developer.android.com/guide/developing/tools/emulator.html). 20 | 21 | Tested Devices 22 | --- 23 | Please help us grow this list by reporting your working devices: 24 | 25 | * Android emulator 26 | * Droid Incredible 27 | * Evo 4G 28 | * Nexus One 29 | * Nexus S 30 | * Galaxy Tab (see also _Known Issues_) 31 | * Xoom 32 | 33 | Requirements 34 | --- 35 | The Android [SDK](http://developer.android.com/sdk/) is necessary to run the application. In particular, the adb tool is required to be in your _$PATH_. 36 | To run the shell recorder, a Python (v2.6+) environment is required. 37 | 38 | How to record 39 | --- 40 | Run the [recorder.py](https://github.com/senchalabs/android-tools/blob/master/eventrecorder/src/shell/recorder.py) file with the test name as an argument, i.e. 41 | 42 | python recorder.py 43 | 44 | The recorder script will then automatically install an android tool on your device. If you wish to bypass this step, you can pass the "-n" option. Next, a prompt will be displayed where you can use a few commands. These are as follows: 45 | 46 | * __s__ or __screen__: Capture screen. 47 | * __t__ or __text__: Input text. 48 | 49 | Any commands not using any of these prefixes will be either interpreted as an URL load (if it starts with _http://_ or _www_) or as JavaScript input, which will be evaluated by the browser. Your first command should always be an URL. 50 | 51 | JavaScript values can be logged using the [console.log()](http://getfirebug.com/wiki/index.php/Console_API#console.log.28object.5B.2C_object.2C_....5D.29) method. 52 | 53 | All touch events and hardware keyboard events will also be recorded in this stage. 54 | 55 | When recording is complete simply press _ENTER_ at the prompt. This will cause the recording to stop and a python script named _testname.py_ should be available in your current directory when the recorder exits. Note that an initial run of the generated playback script is necessary to generate the baseline result. 56 | 57 | How to play 58 | --- 59 | Simply execute the script generated by the recorder: 60 | 61 | python 62 | 63 | The recorded events will then be played back, and all screen captures and the console log will be available in the current directory. They can then be compared against a baseline if wished. 64 | 65 | Detecting Visual Regressions 66 | --- 67 | For convenience, we provide the simple [imagediff.py](https://github.com/extjs/Orchid/blob/master/autotouch/src/shell/imagediff.py) tool. It expects two input screen captures and an output file name. The generated image will represent a grayscale difference between the input images. This tool can be easily incorporated in regression suites to generate fault reports. If the two input images are identical, no output image is generated. The format of the output image will be determined by the file name extension. 68 | 69 | Note: if you need a more complete solution, you might want to consider using the ImageMagick [compare](http://www.imagemagick.org/script/compare.php) tool. 70 | 71 | Detecting Non-Visual Regressions 72 | --- 73 | The other existing mechanism consists of logging JavaScript code results. The _console.log_ output file resulting from a playback contains everything that was logged during the execution of the test. In order to detect regressions, one simply needs to perform a _diff_ between the log present in the baseline and the log that resulted from the latest playback. 74 | 75 | How it works 76 | --- 77 | When the application is started, it automatically installs an Android package ([APK](http://en.wikipedia.org/wiki/APK_\(file_format\))) called _EventRecorder_ on your selected device. If a package already exists, it is uninstalled first. 78 | 79 | The android package is responsible for recording all events the user initiates and write this to a file that is later fetched from the phone. It is also responsible for replaying events from an existing file when told to do so. 80 | 81 | The application on the host is responsible for notifying the android side when the user initiates certain events (loading URLs, capturing screenshots etc) and also has to fetch the event file when recording is complete. When replaying, it also needs to send the event file to the device for execution. 82 | 83 | Known Issues 84 | --- 85 | * Events recorded through soft keyboard typing won't be reproduced in the playback. 86 | * The Samsung Galaxy Tab will always send the framebuffer in landscape mode. This means the user needs to test applications in landscape mode on this device. 87 | * Some event sequences might not be exactly reproduced on playback compared to their results during recording. This is because the timing between the events is not 100% accurate in relation to the originally recorded. In particular, fling scroll might lead to different end offsets. 88 | 89 | Customizing the Android Tool 90 | --- 91 | If you wish to make changes to the Android tool for your own needs you'll need, in addition to the above mentioned requirements, an [Ant](http://ant.apache.org/) environment. Make sure the _android_ application is in your _$PATH_. Once ready, go to the [android](https://github.com/senchalabs/android-tools/blob/master/eventrecorder/src/android/) folder and run the following on your shell: 92 | 93 | android update project -p . 94 | 95 | Please note that the above step only needs to be executed once per workstation. It will generate a file called _local.properties_. 96 | 97 | Next, type: 98 | 99 | ant debug 100 | 101 | A debug package named _bin/EventRecorder-debug.apk_ will be generated. If you wish to sign your application, follow [these steps](http://developer.android.com/guide/publishing/app-signing.html). 102 | 103 | Finally, switch to the [shell](https://github.com/senchalabs/android-tools/blob/master/eventrecorder/src/shell/) directory and run: 104 | 105 | python inline-apk.py 106 | 107 | This will embed the APK in the recorder, so that it doesn't need to be installed manually on all devices. The recorder is now ready to be used. 108 | -------------------------------------------------------------------------------- /eventrecorder/src/android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /eventrecorder/src/android/bin/EventRecorder.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/android/bin/EventRecorder.apk -------------------------------------------------------------------------------- /eventrecorder/src/android/build.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked in Version Control Systems, as it is 4 | # integral to the build system of your project. 5 | 6 | # This file is only used by the Ant script. 7 | 8 | # You can use this to override default values such as 9 | # 'source.dir' for the location of your java source folder and 10 | # 'out.dir' for the location of your output folder. 11 | 12 | # You can also use it define how the release builds are signed by declaring 13 | # the following properties: 14 | # 'key.store' for the location of your keystore and 15 | # 'key.alias' for the name of the key to use. 16 | # The password will be asked during the build when you use the 'release' target. 17 | 18 | -------------------------------------------------------------------------------- /eventrecorder/src/android/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /eventrecorder/src/android/default.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "build.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-7 12 | -------------------------------------------------------------------------------- /eventrecorder/src/android/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/android/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /eventrecorder/src/android/res/raw/template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '%EVENTS%' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, boundary): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop(boundary) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | boundary = (reply['boundaryLeft'], reply['boundaryTop'], 210 | reply['boundaryRight'], reply['boundaryBottom']) 211 | captureScreen(localFileName, boundary) 212 | sendIntent(_INTENT_SCREEN_DONE) 213 | 214 | elif reply['type'] == type: 215 | proc.kill() 216 | clearLogcat() 217 | return reply 218 | 219 | def printUsage(): 220 | app = os.path.basename(sys.argv[0]) 221 | print "Usage: ", app, "\t\t- assume one attached device only" 222 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 223 | print " ", app, _OPTION_HELP, "\t\t- print this help" 224 | 225 | def readData(socket, max = 4096): 226 | return socket.recv(max) 227 | 228 | def readOkay(socket): 229 | data = socket.recv(4) 230 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 231 | 232 | def sendData(socket, str): 233 | return socket.sendall('%04X%s' % (len(str), str)) 234 | 235 | def execute(cmd): 236 | fullCmd = 'adb ' 237 | if targetDevice(): 238 | fullCmd += '-s ' + targetDevice() + ' ' 239 | fullCmd += cmd 240 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 241 | proc.stdin.close() 242 | proc.wait() 243 | 244 | def startAdbServer(): 245 | execute('start-server') 246 | 247 | def query(cmd): 248 | fullCmd = 'adb ' 249 | if targetDevice(): 250 | fullCmd += '-s ' + targetDevice() + ' ' 251 | fullCmd += cmd 252 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 253 | output = proc.stdout.read() 254 | proc.stdin.close() 255 | proc.wait() 256 | return output 257 | 258 | def devices(): 259 | sock = startConnection(_ADB_PORT) 260 | sendData(sock, 'host:devices') 261 | if readOkay(sock): 262 | readData(sock, 4) # payload size in hex 263 | data = readData(sock) 264 | reply = "" 265 | while len(data): 266 | reply += data 267 | data = readData(sock) 268 | sock.close() 269 | devices = re.sub('List of devices attached\s+', '', reply) 270 | devices = devices.splitlines() 271 | list = [] 272 | for elem in devices: 273 | if elem.find('device') != -1: 274 | list.append(re.sub(r'\s*device', '', elem)) 275 | return list 276 | else: # adb server not running 277 | sock.close() 278 | return None 279 | 280 | def isAvailable(): 281 | return query('version').startswith('Android Debug Bridge') 282 | 283 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 284 | clearLogcat() 285 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 286 | if data: 287 | cmd += " -d '" + data + "'" 288 | execute(cmd) 289 | 290 | def pull(remote, local): 291 | execute('pull ' + remote + ' ' + local) 292 | 293 | def push(local, remote): 294 | execute('push ' + local + ' ' + remote) 295 | 296 | def runTest(): 297 | def checkError(r): 298 | error = r['error'] 299 | if error: 300 | setExitCode(ExitCode.Aborted) 301 | setError(error) 302 | raise Exception() 303 | 304 | print "Launching remote application..." 305 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 306 | reply = waitForReply(_REPLY_READY) 307 | checkError(reply) 308 | 309 | print "Sending playback events..." 310 | sendIntent(_INTENT_PLAY) 311 | reply = waitForReply(_REPLY_EVENTS_PATH) 312 | file = tempfile.NamedTemporaryFile() 313 | file.write(_g_events) 314 | file.flush() 315 | 316 | push(file.name, reply["value"]) 317 | file.close() 318 | sendIntent(_INTENT_PUSH_DONE) 319 | 320 | print "Playing test..." 321 | reply = waitForReply(_REPLY_DONE) 322 | checkError(reply) 323 | 324 | prefix = reply['filesPath'] 325 | consoleLogFile = reply['consoleLogFile'] 326 | 327 | print "Fetching results..." 328 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 329 | 330 | print "Done." 331 | 332 | def main(): 333 | args = sys.argv[1:] 334 | 335 | if _OPTION_HELP in args: 336 | printUsage() 337 | return ExitCode.Help 338 | 339 | if not isAvailable(): 340 | print "'adb' not found, please add its location to $PATH." 341 | return ExitCode.AdbNotFound 342 | 343 | startAdbServer() 344 | deviceList = devices() 345 | 346 | if len(deviceList) == 0: 347 | print "No attached devices." 348 | return ExitCode.NoDevices 349 | 350 | if _OPTION_DEVICE in args: 351 | try: 352 | serial = args[args.index(_OPTION_DEVICE) + 1] 353 | except IndexError: 354 | print "Must specify a device serial number." 355 | return ExitCode.WrongUsage 356 | if serial in deviceList: 357 | setTargetDevice(serial) 358 | else: 359 | print "Device " + serial + " not found." 360 | return ExitCode.UnknownDevice 361 | else: 362 | if len(deviceList) > 1: 363 | print "Multiple devices attached, one must be specified." 364 | return ExitCode.MultipleDevices 365 | 366 | print "EventRecorder - Remote Automated Web Application Testing for Android." 367 | if not targetDevice(): 368 | setTargetDevice(deviceList[0]) 369 | 370 | print "Target device is " + targetDevice() + "." 371 | 372 | try: 373 | runTest() 374 | except Exception as e: 375 | print e 376 | code = exitCode() 377 | if code == ExitCode.Normal: 378 | print "Exiting..." 379 | elif code == ExitCode.DeviceDisconnected: 380 | print "Device disconnected." 381 | elif code == ExitCode.Aborted: 382 | print _g_state['error'] 383 | return code 384 | 385 | if __name__ == "__main__": 386 | sys.exit(main()) 387 | -------------------------------------------------------------------------------- /eventrecorder/src/android/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | EventRecorder 4 | 5 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/Base64.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import java.text.ParseException; 26 | 27 | public class Base64 { 28 | public static String encode(String input) { 29 | return ""; 30 | } 31 | 32 | public static String decode(String input) throws ParseException { 33 | int len = ((input.length() + 3) / 4) * 3; 34 | byte[] result = new byte[len]; 35 | int pos = -1; 36 | 37 | char c; 38 | int temp = 0; 39 | for (int i = 0; i < input.length(); ++i) { 40 | c = input.charAt(i); 41 | 42 | temp <<= 6; 43 | if (c >= 'A' && c <= 'Z') 44 | temp |= (byte)(c - 'A'); 45 | else if (c >= 'a' && c <= 'z') 46 | temp |= (byte)(c - 'a' + 26); 47 | else if (c >= '0' && c <= '9') 48 | temp |= (byte)(c - '0' + 52); 49 | else if (c == '+') 50 | temp |= 62; 51 | else if (c == '/') 52 | temp |= 63; 53 | else if (c == '=') { 54 | switch (input.length() - i) { 55 | case 1: 56 | result[++pos] = (byte)((temp >> 16) & 0xff); 57 | result[++pos] = (byte)((temp >> 8) & 0xff); 58 | return new String(result, 0, len - 1); 59 | case 2: 60 | result[++pos] = (byte)((temp >> 10) & 0xff); 61 | return new String(result, 0, len - 2); 62 | default: 63 | throw new ParseException("Invalid number of pad characters", i); 64 | } 65 | } else { 66 | throw new ParseException("Invalid character: " + c, i); 67 | } 68 | if ((i + 1) % 4 == 0) { 69 | result[++pos] = (byte)((temp >> 16) & 0xff); 70 | result[++pos] = (byte)((temp >> 8) & 0xff); 71 | result[++pos] = (byte)(temp & 0xff); 72 | } 73 | } 74 | return new String(result); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/EventWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import android.view.KeyEvent; 26 | import android.view.MotionEvent; 27 | 28 | class EventWriter extends PlaybackFileAccess { 29 | EventWriter(String fileName) { 30 | super(fileName, "w"); 31 | } 32 | 33 | public void writeCommand(long timestamp, String type, String value) { 34 | String text = timestamp + " " + type + " " + escapeQuotes(value) + "\\n"; 35 | writeText(text); 36 | } 37 | 38 | public void writeCommand(long timestamp, String command) { 39 | String text = timestamp + " " + command + "\\n"; 40 | writeText(text); 41 | } 42 | 43 | public void writeTouchEvent(long timestamp, int action, float x, float y, float xPrecision, float yPrecision, 44 | float pressure, float size, int deviceId, int meta, int edge) 45 | { 46 | writeCommand(timestamp, App.COMMAND_TOUCH, 47 | action 48 | + " " + x 49 | + " " + y 50 | + " " + xPrecision 51 | + " " + yPrecision 52 | + " " + pressure 53 | + " " + size 54 | + " " + deviceId 55 | + " " + meta 56 | + " " + edge); 57 | } 58 | 59 | public void writeKeyEvent(long timestamp, int code, int action, int repeat, int meta, 60 | int device, int scan, int flags, String chars) 61 | { 62 | String event = code 63 | + " " + action 64 | + " " + repeat 65 | + " " + meta 66 | + " " + device 67 | + " " + scan 68 | + " " + flags; 69 | 70 | if (chars != null) 71 | event += " " + escapeNewlines(chars); 72 | writeCommand(timestamp, App.COMMAND_KEY, event); 73 | } 74 | 75 | public void writeTextEvent(long timestamp, String text) { 76 | writeCommand(timestamp, App.COMMAND_TEXT, text); 77 | } 78 | 79 | private String escapeQuotes(String text) 80 | { 81 | String newtext = text.replace("\"", "\\\""); 82 | return newtext.replace("'", "\\'"); 83 | } 84 | 85 | private String escapeNewlines(String text) 86 | { 87 | return text.replace("\n", "\\\n"); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/FileAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import android.util.Log; 26 | import java.io.BufferedInputStream; 27 | import java.io.FileInputStream; 28 | import java.io.InputStream; 29 | import java.io.File; 30 | import java.io.FileDescriptor; 31 | import java.io.IOException; 32 | import java.io.RandomAccessFile; 33 | 34 | class FileAccess { 35 | FileAccess(String fileName, String mode) { 36 | try { 37 | File file = new File(fileName); 38 | if (mode.equals("w")) { 39 | file.delete(); 40 | mode = "rw"; 41 | } 42 | mFile = new RandomAccessFile(file, mode); 43 | mFileName = fileName; 44 | } catch (IOException e) { 45 | Log.i(App.LOGTAG, Reply.error("Unable to create file " + fileName)); 46 | } 47 | } 48 | 49 | public String getFileName() { 50 | return mFileName; 51 | } 52 | 53 | public FileDescriptor getFileDescriptor() { 54 | try { 55 | return mFile.getFD(); 56 | } catch (IOException e) { 57 | } 58 | return null; 59 | } 60 | 61 | public String readAll() { 62 | try { 63 | return readAll(new FileInputStream(mFile.getFD())); 64 | } catch (IOException e) { 65 | return null; 66 | } 67 | } 68 | 69 | protected String readAll(InputStream reader) { 70 | try { 71 | String data = new String(); 72 | 73 | byte[] buf = new byte[4096]; 74 | int rd; 75 | do { 76 | rd = reader.read(buf, 0, 4096); 77 | if (rd > 0) 78 | data += new String(buf, 0, rd); 79 | } while (rd != -1); 80 | 81 | return data; 82 | } catch (IOException e) { 83 | Log.i(App.LOGTAG, Reply.error("Error when reading file " + mFileName)); 84 | } 85 | 86 | return null; 87 | } 88 | 89 | public void writeText(String text) { 90 | try { 91 | mFile.writeBytes(text); 92 | } catch (IOException e) { 93 | Log.i(App.LOGTAG, Reply.error("Unable to write to file " + mFileName)); 94 | } 95 | } 96 | 97 | public void close() { 98 | try { 99 | mFile.close(); 100 | } catch (IOException e) { 101 | Log.i(App.LOGTAG, Reply.error("Unable to close file " + mFileName)); 102 | } 103 | } 104 | 105 | private RandomAccessFile mFile; 106 | private String mFileName; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/HandlerTimer.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import android.os.Handler; 26 | 27 | class HandlerTimer { 28 | HandlerTimer() { 29 | if (mHandler == null) 30 | mHandler = new Handler(); 31 | } 32 | 33 | public boolean schedule(Runnable r, long delayMillis) { 34 | return mHandler.postDelayed(r, delayMillis); 35 | } 36 | 37 | public boolean scheduleAt(Runnable r, long uptimeMillis) { 38 | return mHandler.postAtTime(r, uptimeMillis); 39 | } 40 | 41 | public void cancel(Runnable r) { 42 | mHandler.removeCallbacks(r); 43 | } 44 | 45 | private static Handler mHandler = null; 46 | } 47 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/PlaybackFileAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import java.io.InputStream; 26 | import java.io.IOException; 27 | 28 | class PlaybackFileAccess extends FileAccess { 29 | PlaybackFileAccess(String fileName, String mode) { 30 | super(fileName, mode); 31 | InputStream template = App.getSingletonInstance().getResources().openRawResource(R.raw.template); 32 | String program = readAll(template); 33 | mTemplate = program.split("%EVENTS%"); 34 | } 35 | 36 | public void writeHeader() { 37 | String header = mTemplate[0]; 38 | writeText(header); 39 | } 40 | 41 | public void writeFooter() { 42 | String footer = mTemplate[1]; 43 | writeText(footer); 44 | } 45 | 46 | private String[] mTemplate; 47 | } 48 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/Reply.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import android.graphics.Rect; 26 | 27 | class Reply { 28 | static public String error(String error) { 29 | return "{'error': '" + error + "'}"; 30 | } 31 | 32 | static public String eventsFilePath(String path) { 33 | return "{'type': 'eventsFilePath', 'value': '" + path + "', 'error': ''}"; 34 | } 35 | 36 | static public String ready() { 37 | return "{'type': 'ready', 'error': ''}"; 38 | } 39 | 40 | static public String screen(Rect boundary) { 41 | return "{'type': 'screen', 'error': ''" 42 | + ", 'boundaryLeft': " + boundary.left 43 | + ", 'boundaryTop': " + boundary.top 44 | + ", 'boundaryRight': " + boundary.right 45 | + ", 'boundaryBottom': " + boundary.bottom 46 | + "}"; 47 | } 48 | 49 | static public String done(String filesPath) { 50 | return "{'type': 'done', 'error': '', " 51 | + "'filesPath': '" + filesPath + "', " 52 | + "'consoleLogFile': '" + App.CONSOLE_LOG + "', " 53 | + "'testScriptFile': '" + App.PLAYBACK_FILE + "'}"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /eventrecorder/src/android/src/com/sencha/eventrecorder/Storage.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.eventrecorder; 24 | 25 | import android.os.Environment; 26 | import java.io.File; 27 | 28 | class Storage { 29 | static public String getDirectory() { 30 | File path; 31 | String state = Environment.getExternalStorageState(); 32 | if (Environment.MEDIA_MOUNTED.equals(state)) { 33 | path = new File(Environment.getExternalStorageDirectory(), 34 | "Android/data/" + App.getSingletonInstance().getPackageName()); 35 | path.mkdirs(); 36 | } else 37 | path = App.getSingletonInstance().getCacheDir(); 38 | return path.getAbsolutePath(); 39 | } 40 | 41 | static public String getAbsoluteFilePath(String fileName) { 42 | return getDirectory() + "/" + fileName; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/forms/nexusone/froyo/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/forms/nexusone/froyo/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/forms/nexusone/gingerbread/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/forms/nexusone/gingerbread/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/icons/evo4g/froyo/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/icons/evo4g/froyo/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/icons/evo4g/froyo/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/icons/evo4g/froyo/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/overlays/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/overlays/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/overlays/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/overlays/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/overlays/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/overlays/screen3.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/overlays/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/overlays/screen4.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/picker/nexusone/gingerbread/console.log: -------------------------------------------------------------------------------- 1 | Mon Jun 27 1988 00:00:00 GMT-0700 (PDT) 2 | Mon Jun 27 1988 00:00:00 GMT-0700 (PDT) 3 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen3.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen3.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen4.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen5.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen1.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen2.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen3.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen4.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen5.png -------------------------------------------------------------------------------- /eventrecorder/src/examples/tests/carousel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '0 url http://dev.sencha.com/deploy/touch/examples/carousel/\n0 pause\n4091 screen\n5271 touch 0 389.01175 101.54253 0.0 0.0 0.2627451 0.3 65541 0 0\n5300 touch 2 383.38748 101.54253 0.0 0.0 0.2627451 0.3 65541 0 0\n5315 touch 2 316.83365 104.02905 0.0 0.0 0.2627451 0.25 65541 0 0\n5347 touch 2 253.56067 104.02905 0.0 0.0 0.2627451 0.3 65541 0 0\n5379 touch 2 202.0049 104.02905 0.0 0.0 0.105882354 0.2 65541 0 0\n5421 touch 1 202.0049 104.02905 0.0 0.0 0.105882354 0.2 65541 0 0\n6576 touch 0 427.9129 90.76764 0.0 0.0 0.21960784 0.25 65541 0 0\n6610 touch 2 414.7896 89.9388 0.0 0.0 0.2509804 0.25 65541 0 0\n6626 touch 2 355.73483 90.76764 0.0 0.0 0.2509804 0.35 65541 0 0\n6658 touch 2 300.42953 89.109955 0.0 0.0 0.2784314 0.3 65541 0 0\n6689 touch 2 246.99902 89.109955 0.0 0.0 0.2509804 0.35 65541 0 0\n6721 touch 2 207.62915 93.25415 0.0 0.0 0.1764706 0.15 65541 0 0\n6763 touch 1 207.62915 93.25415 0.0 0.0 0.1764706 0.15 65541 0 0\n9355 touch 0 250.27983 449.65454 0.0 0.0 0.19215687 0.25 65541 0 0\n9420 touch 2 248.40508 465.40253 0.0 0.0 0.19215687 0.2 65541 0 0\n9437 touch 2 246.53033 492.75415 0.0 0.0 0.19215687 0.2 65541 0 0\n9467 touch 2 246.06165 553.25934 0.0 0.0 0.19215687 0.15 65541 0 0\n9498 touch 2 248.87376 582.2687 0.0 0.0 0.16078432 0.15 65541 0 0\n9530 touch 2 254.02936 605.47614 0.0 0.0 0.11764706 0.1 65541 0 0\n9572 touch 1 254.02936 605.47614 0.0 0.0 0.11764706 0.1 65541 0 0\n12395 screen\n' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, skipLines = 0): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop((0, skipLines, width - 1, height - 1)) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | skipLines = reply['skipLines'] 210 | captureScreen(localFileName, skipLines) 211 | sendIntent(_INTENT_SCREEN_DONE) 212 | 213 | elif reply['type'] == type: 214 | proc.kill() 215 | clearLogcat() 216 | return reply 217 | 218 | def printUsage(): 219 | app = os.path.basename(sys.argv[0]) 220 | print "Usage: ", app, "\t\t- assume one attached device only" 221 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 222 | print " ", app, _OPTION_HELP, "\t\t- print this help" 223 | 224 | def readData(socket, max = 4096): 225 | return socket.recv(max) 226 | 227 | def readOkay(socket): 228 | data = socket.recv(4) 229 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 230 | 231 | def sendData(socket, str): 232 | return socket.sendall('%04X%s' % (len(str), str)) 233 | 234 | def execute(cmd): 235 | fullCmd = 'adb ' 236 | if targetDevice(): 237 | fullCmd += '-s ' + targetDevice() + ' ' 238 | fullCmd += cmd 239 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 240 | proc.stdin.close() 241 | proc.wait() 242 | 243 | def startAdbServer(): 244 | execute('start-server') 245 | 246 | def query(cmd): 247 | fullCmd = 'adb ' 248 | if targetDevice(): 249 | fullCmd += '-s ' + targetDevice() + ' ' 250 | fullCmd += cmd 251 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 252 | output = proc.stdout.read() 253 | proc.stdin.close() 254 | proc.wait() 255 | return output 256 | 257 | def devices(): 258 | sock = startConnection(_ADB_PORT) 259 | sendData(sock, 'host:devices') 260 | if readOkay(sock): 261 | readData(sock, 4) # payload size in hex 262 | data = readData(sock) 263 | reply = "" 264 | while len(data): 265 | reply += data 266 | data = readData(sock) 267 | sock.close() 268 | devices = re.sub('List of devices attached\s+', '', reply) 269 | devices = devices.splitlines() 270 | list = [] 271 | for elem in devices: 272 | if elem.find('device') != -1: 273 | list.append(re.sub(r'\s*device', '', elem)) 274 | return list 275 | else: # adb server not running 276 | sock.close() 277 | return None 278 | 279 | def isAvailable(): 280 | return query('version').startswith('Android Debug Bridge') 281 | 282 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 283 | clearLogcat() 284 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 285 | if data: 286 | cmd += " -d '" + data + "'" 287 | execute(cmd) 288 | 289 | def pull(remote, local): 290 | execute('pull ' + remote + ' ' + local) 291 | 292 | def push(local, remote): 293 | execute('push ' + local + ' ' + remote) 294 | 295 | def runTest(): 296 | def checkError(r): 297 | error = r['error'] 298 | if error: 299 | setExitCode(ExitCode.Aborted) 300 | setError(error) 301 | raise Exception() 302 | 303 | print "Launching remote application..." 304 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 305 | reply = waitForReply(_REPLY_READY) 306 | checkError(reply) 307 | 308 | print "Sending playback events..." 309 | sendIntent(_INTENT_PLAY) 310 | reply = waitForReply(_REPLY_EVENTS_PATH) 311 | file = tempfile.NamedTemporaryFile() 312 | file.write(_g_events) 313 | file.flush() 314 | 315 | push(file.name, reply["value"]) 316 | file.close() 317 | sendIntent(_INTENT_PUSH_DONE) 318 | 319 | print "Playing test..." 320 | reply = waitForReply(_REPLY_DONE) 321 | checkError(reply) 322 | 323 | prefix = reply['filesPath'] 324 | consoleLogFile = reply['consoleLogFile'] 325 | 326 | print "Fetching results..." 327 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 328 | 329 | print "Done." 330 | 331 | def main(): 332 | args = sys.argv[1:] 333 | 334 | if _OPTION_HELP in args: 335 | printUsage() 336 | return ExitCode.Help 337 | 338 | if not isAvailable(): 339 | print "'adb' not found, please add its location to $PATH." 340 | return ExitCode.AdbNotFound 341 | 342 | startAdbServer() 343 | deviceList = devices() 344 | 345 | if len(deviceList) == 0: 346 | print "No attached devices." 347 | return ExitCode.NoDevices 348 | 349 | if _OPTION_DEVICE in args: 350 | try: 351 | serial = args[args.index(_OPTION_DEVICE) + 1] 352 | except IndexError: 353 | print "Must specify a device serial number." 354 | return ExitCode.WrongUsage 355 | if serial in deviceList: 356 | setTargetDevice(serial) 357 | else: 358 | print "Device " + serial + " not found." 359 | return ExitCode.UnknownDevice 360 | else: 361 | if len(deviceList) > 1: 362 | print "Multiple devices attached, one must be specified." 363 | return ExitCode.MultipleDevices 364 | 365 | print "EventRecorder - Remote Automated Web Application Testing for Android." 366 | if not targetDevice(): 367 | setTargetDevice(deviceList[0]) 368 | 369 | print "Target device is " + targetDevice() + "." 370 | 371 | try: 372 | runTest() 373 | except Exception as e: 374 | print e 375 | code = exitCode() 376 | if code == ExitCode.Normal: 377 | print "Exiting..." 378 | elif code == ExitCode.DeviceDisconnected: 379 | print "Device disconnected." 380 | elif code == ExitCode.Aborted: 381 | print _g_state['error'] 382 | return code 383 | 384 | if __name__ == "__main__": 385 | sys.exit(main()) 386 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/tests/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '0 url http://dev.sencha.com/deploy/touch/examples/forms\n0 pause\n2077 touch 0 337.44077 131.49135 7.9125 7.9424996 0.47 0.06666667 131074 0 0\n2088 touch 2 337.31436 131.49135 7.9125 7.9424996 0.48999998 0.06666667 131074 0 0\n2113 touch 2 337.6935 132.7504 7.9125 7.9424996 0.5 0.06666667 131074 0 0\n2162 touch 2 338.83096 133.88354 7.9125 7.9424996 0.26 0.13333334 131074 0 0\n2173 touch 1 338.83096 133.88354 7.9125 7.9424996 0.26 0.13333334 131074 0 0\n4968 text John Smith\n6148 touch 0 329.60504 215.72177 7.9125 7.9424996 0.42999998 0.06666667 131074 0 0\n6161 touch 2 329.73145 215.84766 7.9125 7.9424996 0.47 0.06666667 131074 0 0\n6289 touch 1 329.22592 215.84766 7.9125 7.9424996 0.17 0.06666667 131074 0 0\n10287 text Mypassword\n11348 touch 0 343.25433 366.05228 7.9125 7.9424996 0.45999998 0.06666667 131074 0 0\n11362 touch 2 343.25433 365.92636 7.9125 7.9424996 0.47 0.06666667 131074 0 0\n11441 touch 2 345.40283 367.3113 7.9125 7.9424996 0.21 0.13333334 131074 0 0\n11455 touch 1 345.40283 367.3113 7.9125 7.9424996 0.21 0.13333334 131074 0 0\n15207 text me@sencha\n16565 touch 0 340.6003 420.94684 7.9125 7.9424996 0.26999998 0.06666667 131074 0 0\n16580 touch 2 340.8531 421.07272 7.9125 7.9424996 0.31 0.06666667 131074 0 0\n16609 touch 2 340.22116 422.45767 7.9125 7.9424996 0.45 0.06666667 131074 0 0\n16632 touch 2 339.2101 422.45767 7.9125 7.9424996 0.47 0.06666667 131074 0 0\n16667 touch 2 340.47394 422.3318 7.9125 7.9424996 0.26 0.13333334 131074 0 0\n16691 touch 1 339.9684 422.7095 7.9125 7.9424996 0.14 0.13333334 131074 0 0\n20561 text www.sencha.com\n22542 touch 0 422.62244 562.0863 7.9125 7.9424996 0.39 0.06666667 131074 0 0\n22552 touch 2 422.7488 562.21216 7.9125 7.9424996 0.39999998 0.06666667 131074 0 0\n22654 touch 2 423.5071 563.34534 7.9125 7.9424996 0.41 0.06666667 131074 0 0\n22677 touch 2 424.39178 564.47845 7.9125 7.9424996 0.17999999 0.06666667 131074 0 0\n22691 touch 1 424.39178 564.47845 7.9125 7.9424996 0.17999999 0.06666667 131074 0 0\n23456 touch 0 423.12796 558.30914 7.9125 7.9424996 0.37 0.06666667 131074 0 0\n23471 touch 2 423.25433 557.9314 7.9125 7.9424996 0.39 0.06666667 131074 0 0\n23498 touch 2 423.7599 558.93866 7.9125 7.9424996 0.39999998 0.06666667 131074 0 0\n23544 touch 2 423.88626 560.4495 7.9125 7.9424996 0.39999998 0.06666667 131074 0 0\n23614 touch 1 424.01263 560.7013 7.9125 7.9424996 0.17 0.06666667 131074 0 0\n25194 touch 0 328.21484 558.93866 7.9125 7.9424996 0.38 0.06666667 131074 0 0\n25203 touch 2 328.21484 559.31635 7.9125 7.9424996 0.45 0.06666667 131074 0 0\n25237 touch 2 327.2038 560.1977 7.9125 7.9424996 0.48999998 0.06666667 131074 0 0\n25307 touch 2 328.34122 559.31635 7.9125 7.9424996 0.48999998 0.06666667 131074 0 0\n25344 touch 1 329.22592 558.5609 7.9125 7.9424996 0.21 0.06666667 131074 0 0\n29640 text 25\n33442 touch 0 333.14377 503.41455 7.9125 7.9424996 0.39999998 0.06666667 131074 0 0\n33452 touch 2 332.76462 503.7923 7.9125 7.9424996 0.42999998 0.06666667 131074 0 0\n33543 touch 2 333.3965 504.9254 7.9125 7.9424996 0.25 0.06666667 131074 0 0\n33555 touch 1 333.3965 504.9254 7.9125 7.9424996 0.25 0.06666667 131074 0 0\n34031 screen\n' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, skipLines = 0): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop((0, skipLines, width - 1, height - 1)) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | skipLines = reply['skipLines'] 210 | captureScreen(localFileName, skipLines) 211 | sendIntent(_INTENT_SCREEN_DONE) 212 | 213 | elif reply['type'] == type: 214 | proc.kill() 215 | clearLogcat() 216 | return reply 217 | 218 | def printUsage(): 219 | app = os.path.basename(sys.argv[0]) 220 | print "Usage: ", app, "\t\t- assume one attached device only" 221 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 222 | print " ", app, _OPTION_HELP, "\t\t- print this help" 223 | 224 | def readData(socket, max = 4096): 225 | return socket.recv(max) 226 | 227 | def readOkay(socket): 228 | data = socket.recv(4) 229 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 230 | 231 | def sendData(socket, str): 232 | return socket.sendall('%04X%s' % (len(str), str)) 233 | 234 | def execute(cmd): 235 | fullCmd = 'adb ' 236 | if targetDevice(): 237 | fullCmd += '-s ' + targetDevice() + ' ' 238 | fullCmd += cmd 239 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 240 | proc.stdin.close() 241 | proc.wait() 242 | 243 | def startAdbServer(): 244 | execute('start-server') 245 | 246 | def query(cmd): 247 | fullCmd = 'adb ' 248 | if targetDevice(): 249 | fullCmd += '-s ' + targetDevice() + ' ' 250 | fullCmd += cmd 251 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 252 | output = proc.stdout.read() 253 | proc.stdin.close() 254 | proc.wait() 255 | return output 256 | 257 | def devices(): 258 | sock = startConnection(_ADB_PORT) 259 | sendData(sock, 'host:devices') 260 | if readOkay(sock): 261 | readData(sock, 4) # payload size in hex 262 | data = readData(sock) 263 | reply = "" 264 | while len(data): 265 | reply += data 266 | data = readData(sock) 267 | sock.close() 268 | devices = re.sub('List of devices attached\s+', '', reply) 269 | devices = devices.splitlines() 270 | list = [] 271 | for elem in devices: 272 | if elem.find('device') != -1: 273 | list.append(re.sub(r'\s*device', '', elem)) 274 | return list 275 | else: # adb server not running 276 | sock.close() 277 | return None 278 | 279 | def isAvailable(): 280 | return query('version').startswith('Android Debug Bridge') 281 | 282 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 283 | clearLogcat() 284 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 285 | if data: 286 | cmd += " -d '" + data + "'" 287 | execute(cmd) 288 | 289 | def pull(remote, local): 290 | execute('pull ' + remote + ' ' + local) 291 | 292 | def push(local, remote): 293 | execute('push ' + local + ' ' + remote) 294 | 295 | def runTest(): 296 | def checkError(r): 297 | error = r['error'] 298 | if error: 299 | setExitCode(ExitCode.Aborted) 300 | setError(error) 301 | raise Exception() 302 | 303 | print "Launching remote application..." 304 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 305 | reply = waitForReply(_REPLY_READY) 306 | checkError(reply) 307 | 308 | print "Sending playback events..." 309 | sendIntent(_INTENT_PLAY) 310 | reply = waitForReply(_REPLY_EVENTS_PATH) 311 | file = tempfile.NamedTemporaryFile() 312 | file.write(_g_events) 313 | file.flush() 314 | 315 | push(file.name, reply["value"]) 316 | file.close() 317 | sendIntent(_INTENT_PUSH_DONE) 318 | 319 | print "Playing test..." 320 | reply = waitForReply(_REPLY_DONE) 321 | checkError(reply) 322 | 323 | prefix = reply['filesPath'] 324 | consoleLogFile = reply['consoleLogFile'] 325 | 326 | print "Fetching results..." 327 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 328 | 329 | print "Done." 330 | 331 | def main(): 332 | args = sys.argv[1:] 333 | 334 | if _OPTION_HELP in args: 335 | printUsage() 336 | return ExitCode.Help 337 | 338 | if not isAvailable(): 339 | print "'adb' not found, please add its location to $PATH." 340 | return ExitCode.AdbNotFound 341 | 342 | startAdbServer() 343 | deviceList = devices() 344 | 345 | if len(deviceList) == 0: 346 | print "No attached devices." 347 | return ExitCode.NoDevices 348 | 349 | if _OPTION_DEVICE in args: 350 | try: 351 | serial = args[args.index(_OPTION_DEVICE) + 1] 352 | except IndexError: 353 | print "Must specify a device serial number." 354 | return ExitCode.WrongUsage 355 | if serial in deviceList: 356 | setTargetDevice(serial) 357 | else: 358 | print "Device " + serial + " not found." 359 | return ExitCode.UnknownDevice 360 | else: 361 | if len(deviceList) > 1: 362 | print "Multiple devices attached, one must be specified." 363 | return ExitCode.MultipleDevices 364 | 365 | print "EventRecorder - Remote Automated Web Application Testing for Android." 366 | if not targetDevice(): 367 | setTargetDevice(deviceList[0]) 368 | 369 | print "Target device is " + targetDevice() + "." 370 | 371 | try: 372 | runTest() 373 | except Exception as e: 374 | print e 375 | code = exitCode() 376 | if code == ExitCode.Normal: 377 | print "Exiting..." 378 | elif code == ExitCode.DeviceDisconnected: 379 | print "Device disconnected." 380 | elif code == ExitCode.Aborted: 381 | print _g_state['error'] 382 | return code 383 | 384 | if __name__ == "__main__": 385 | sys.exit(main()) 386 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/tests/icons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '0 url http://dev.sencha.com/deploy/touch/examples/icons/\n0 pause\n5859 screen\n7696 touch 0 159.83368 682.83636 0.0 0.0 0.21176471 0.2 65538 0 0\n7811 touch 1 159.83368 682.83636 0.0 0.0 0.21176471 0.2 65538 0 0\n10024 touch 0 121.75418 697.36365 0.0 0.0 0.1764706 0.2 65538 0 0\n10055 touch 2 121.75418 697.36365 0.0 0.0 0.22745098 0.25 65538 0 0\n10240 touch 1 121.75418 697.36365 0.0 0.0 0.18431373 0.25 65538 0 0\n11671 touch 0 258.0387 679.41815 0.0 0.0 0.21176471 0.3 65538 0 0\n11731 touch 2 258.0387 679.41815 0.0 0.0 0.25490198 0.3 65538 0 0\n11853 touch 1 258.0387 679.41815 0.0 0.0 0.09803922 0.1 65538 0 0\n12627 touch 0 356.74478 694.8 0.0 0.0 0.22352941 0.3 65538 0 0\n12794 touch 1 356.74478 694.8 0.0 0.0 0.22352941 0.3 65538 0 0\n13533 touch 0 441.92258 687.1091 0.0 0.0 0.1882353 0.2 65538 0 0\n13658 touch 2 441.92258 687.1091 0.0 0.0 0.0627451 0.05 65538 0 0\n13702 touch 1 441.92258 687.1091 0.0 0.0 0.0627451 0.05 65538 0 0\n15992 touch 0 434.90796 43.63636 0.0 0.0 0.22352941 0.3 65538 0 0\n16119 touch 2 434.90796 43.63636 0.0 0.0 0.14117648 0.15 65538 0 0\n16162 touch 1 434.90796 43.63636 0.0 0.0 0.14117648 0.15 65538 0 0\n17293 touch 0 361.75522 36.800003 0.0 0.0 0.24313726 0.3 65538 0 0\n17403 touch 2 361.75522 36.800003 0.0 0.0 0.12156863 0.1 65538 0 0\n17446 touch 1 361.75522 36.800003 0.0 0.0 0.12156863 0.1 65538 0 0\n18079 touch 0 303.13284 29.963638 0.0 0.0 0.25882354 0.3 65538 0 0\n18246 touch 1 303.13284 29.963638 0.0 0.0 0.25882354 0.3 65538 0 0\n18798 touch 0 237.99687 45.34545 0.0 0.0 0.23921569 0.2 65538 0 0\n18907 touch 2 237.99687 45.34545 0.0 0.0 0.17254902 0.2 65538 0 0\n18951 touch 1 237.99687 45.34545 0.0 0.0 0.17254902 0.2 65538 0 0\n19502 touch 0 170.85669 37.65455 0.0 0.0 0.27058825 0.35 65538 0 0\n19596 touch 2 170.85669 37.65455 0.0 0.0 0.09019608 0.05 65538 0 0\n19640 touch 1 170.85669 37.65455 0.0 0.0 0.09019608 0.05 65538 0 0\n20192 touch 0 117.74582 34.236366 0.0 0.0 0.24705882 0.35 65538 0 0\n20302 touch 2 117.74582 34.236366 0.0 0.0 0.07058824 0.05 65538 0 0\n20345 touch 1 117.74582 34.236366 0.0 0.0 0.07058824 0.05 65538 0 0\n20944 touch 0 54.614017 39.363632 0.0 0.0 0.2509804 0.3 65538 0 0\n20990 touch 2 54.614017 39.363632 0.0 0.0 0.29411766 0.35 65538 0 0\n21113 touch 1 54.614017 39.363632 0.0 0.0 0.0627451 0.05 65538 0 0\n21617 touch 0 293.11194 282.05453 0.0 0.0 0.1764706 0.15 65538 0 0\n21664 touch 2 293.11194 282.05453 0.0 0.0 0.21960784 0.2 65538 0 0\n21713 touch 2 287.09937 281.2 0.0 0.0 0.21960784 0.3 65538 0 0\n21743 touch 2 238.99895 278.63638 0.0 0.0 0.21960784 0.25 65538 0 0\n21761 touch 2 225.4707 278.63638 0.0 0.0 0.21960784 0.3 65538 0 0\n21789 touch 2 198.41423 283.76364 0.0 0.0 0.21960784 0.2 65538 0 0\n21821 touch 2 179.87552 286.32727 0.0 0.0 0.21960784 0.25 65538 0 0\n21852 touch 2 165.34518 288.03638 0.0 0.0 0.21960784 0.25 65538 0 0\n21884 touch 2 157.32845 288.89093 0.0 0.0 0.21960784 0.2 65538 0 0\n21900 touch 2 155.32426 289.74545 0.0 0.0 0.21960784 0.25 65538 0 0\n21930 touch 2 144.30125 289.74545 0.0 0.0 0.21960784 0.3 65538 0 0\n21962 touch 2 123.25732 288.89093 0.0 0.0 0.21960784 0.25 65538 0 0\n21993 touch 2 117.74582 288.89093 0.0 0.0 0.21960784 0.25 65538 0 0\n22011 touch 2 112.73535 287.18182 0.0 0.0 0.21960784 0.25 65538 0 0\n22042 touch 2 102.71443 285.47272 0.0 0.0 0.21960784 0.25 65538 0 0\n22071 touch 2 98.70607 282.9091 0.0 0.0 0.21960784 0.35 65538 0 0\n22135 touch 2 103.215485 282.05453 0.0 0.0 0.21960784 0.3 65538 0 0\n22152 touch 2 137.2866 277.78183 0.0 0.0 0.21960784 0.3 65538 0 0\n22182 touch 2 188.39331 276.07272 0.0 0.0 0.21960784 0.3 65538 0 0\n22212 touch 2 222.96547 272.65454 0.0 0.0 0.21960784 0.25 65538 0 0\n22244 touch 2 238.4979 271.80002 0.0 0.0 0.21960784 0.3 65538 0 0\n22275 touch 2 240.5021 272.65454 0.0 0.0 0.21960784 0.35 65538 0 0\n22292 touch 2 241.5042 274.36365 0.0 0.0 0.21960784 0.3 65538 0 0\n22322 touch 2 244.00943 317.0909 0.0 0.0 0.21960784 0.2 65538 0 0\n22353 touch 2 236.99477 368.36362 0.0 0.0 0.16862746 0.15 65538 0 0\n22385 touch 2 230.48117 398.2727 0.0 0.0 0.16862746 0.2 65538 0 0\n22402 touch 2 228.97804 411.94547 0.0 0.0 0.1254902 0.15 65538 0 0\n22432 touch 2 225.97176 419.63635 0.0 0.0 0.078431375 0.1 65538 0 0\n22478 touch 1 225.97176 419.63635 0.0 0.0 0.078431375 0.1 65538 0 0\n24109 touch 0 124.259415 34.236366 0.0 0.0 0.26666668 0.35 65538 0 0\n24360 touch 2 121.253136 35.090904 0.0 0.0 0.26666668 0.4 65538 0 0\n24421 touch 2 120.25105 35.090904 0.0 0.0 0.26666668 0.35 65538 0 0\n24625 touch 2 119.24895 35.090904 0.0 0.0 0.26666668 0.35 65538 0 0\n25741 touch 2 118.24686 35.94545 0.0 0.0 0.26666668 0.4 65538 0 0\n26728 touch 2 117.24477 36.800003 0.0 0.0 0.26666668 0.4 65538 0 0\n26944 touch 2 116.242676 36.800003 0.0 0.0 0.30980393 0.4 65538 0 0\n27116 screen\n27210 touch 2 115.74164 38.509094 0.0 0.0 0.30980393 0.4 65538 0 0\n27478 touch 2 114.73954 38.509094 0.0 0.0 0.30980393 0.4 65538 0 0\n27695 touch 1 114.73954 38.509094 0.0 0.0 0.25490198 0.35 65538 0 0\n' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, skipLines = 0): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop((0, skipLines, width - 1, height - 1)) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | skipLines = reply['skipLines'] 210 | captureScreen(localFileName, skipLines) 211 | sendIntent(_INTENT_SCREEN_DONE) 212 | 213 | elif reply['type'] == type: 214 | proc.kill() 215 | clearLogcat() 216 | return reply 217 | 218 | def printUsage(): 219 | app = os.path.basename(sys.argv[0]) 220 | print "Usage: ", app, "\t\t- assume one attached device only" 221 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 222 | print " ", app, _OPTION_HELP, "\t\t- print this help" 223 | 224 | def readData(socket, max = 4096): 225 | return socket.recv(max) 226 | 227 | def readOkay(socket): 228 | data = socket.recv(4) 229 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 230 | 231 | def sendData(socket, str): 232 | return socket.sendall('%04X%s' % (len(str), str)) 233 | 234 | def execute(cmd): 235 | fullCmd = 'adb ' 236 | if targetDevice(): 237 | fullCmd += '-s ' + targetDevice() + ' ' 238 | fullCmd += cmd 239 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 240 | proc.stdin.close() 241 | proc.wait() 242 | 243 | def startAdbServer(): 244 | execute('start-server') 245 | 246 | def query(cmd): 247 | fullCmd = 'adb ' 248 | if targetDevice(): 249 | fullCmd += '-s ' + targetDevice() + ' ' 250 | fullCmd += cmd 251 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 252 | output = proc.stdout.read() 253 | proc.stdin.close() 254 | proc.wait() 255 | return output 256 | 257 | def devices(): 258 | sock = startConnection(_ADB_PORT) 259 | sendData(sock, 'host:devices') 260 | if readOkay(sock): 261 | readData(sock, 4) # payload size in hex 262 | data = readData(sock) 263 | reply = "" 264 | while len(data): 265 | reply += data 266 | data = readData(sock) 267 | sock.close() 268 | devices = re.sub('List of devices attached\s+', '', reply) 269 | devices = devices.splitlines() 270 | list = [] 271 | for elem in devices: 272 | if elem.find('device') != -1: 273 | list.append(re.sub(r'\s*device', '', elem)) 274 | return list 275 | else: # adb server not running 276 | sock.close() 277 | return None 278 | 279 | def isAvailable(): 280 | return query('version').startswith('Android Debug Bridge') 281 | 282 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 283 | clearLogcat() 284 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 285 | if data: 286 | cmd += " -d '" + data + "'" 287 | execute(cmd) 288 | 289 | def pull(remote, local): 290 | execute('pull ' + remote + ' ' + local) 291 | 292 | def push(local, remote): 293 | execute('push ' + local + ' ' + remote) 294 | 295 | def runTest(): 296 | def checkError(r): 297 | error = r['error'] 298 | if error: 299 | setExitCode(ExitCode.Aborted) 300 | setError(error) 301 | raise Exception() 302 | 303 | print "Launching remote application..." 304 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 305 | reply = waitForReply(_REPLY_READY) 306 | checkError(reply) 307 | 308 | print "Sending playback events..." 309 | sendIntent(_INTENT_PLAY) 310 | reply = waitForReply(_REPLY_EVENTS_PATH) 311 | file = tempfile.NamedTemporaryFile() 312 | file.write(_g_events) 313 | file.flush() 314 | 315 | push(file.name, reply["value"]) 316 | file.close() 317 | sendIntent(_INTENT_PUSH_DONE) 318 | 319 | print "Playing test..." 320 | reply = waitForReply(_REPLY_DONE) 321 | checkError(reply) 322 | 323 | prefix = reply['filesPath'] 324 | consoleLogFile = reply['consoleLogFile'] 325 | 326 | print "Fetching results..." 327 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 328 | 329 | print "Done." 330 | 331 | def main(): 332 | args = sys.argv[1:] 333 | 334 | if _OPTION_HELP in args: 335 | printUsage() 336 | return ExitCode.Help 337 | 338 | if not isAvailable(): 339 | print "'adb' not found, please add its location to $PATH." 340 | return ExitCode.AdbNotFound 341 | 342 | startAdbServer() 343 | deviceList = devices() 344 | 345 | if len(deviceList) == 0: 346 | print "No attached devices." 347 | return ExitCode.NoDevices 348 | 349 | if _OPTION_DEVICE in args: 350 | try: 351 | serial = args[args.index(_OPTION_DEVICE) + 1] 352 | except IndexError: 353 | print "Must specify a device serial number." 354 | return ExitCode.WrongUsage 355 | if serial in deviceList: 356 | setTargetDevice(serial) 357 | else: 358 | print "Device " + serial + " not found." 359 | return ExitCode.UnknownDevice 360 | else: 361 | if len(deviceList) > 1: 362 | print "Multiple devices attached, one must be specified." 363 | return ExitCode.MultipleDevices 364 | 365 | print "EventRecorder - Remote Automated Web Application Testing for Android." 366 | if not targetDevice(): 367 | setTargetDevice(deviceList[0]) 368 | 369 | print "Target device is " + targetDevice() + "." 370 | 371 | try: 372 | runTest() 373 | except Exception as e: 374 | print e 375 | code = exitCode() 376 | if code == ExitCode.Normal: 377 | print "Exiting..." 378 | elif code == ExitCode.DeviceDisconnected: 379 | print "Device disconnected." 380 | elif code == ExitCode.Aborted: 381 | print _g_state['error'] 382 | return code 383 | 384 | if __name__ == "__main__": 385 | sys.exit(main()) 386 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/tests/overlays.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '0 url http://dev.sencha.com/deploy/touch/examples/overlays/\n0 pause\n4139 screen\n5819 touch 0 99.5079 721.1138 0.0 0.0 0.23921569 0.06666667 65540 0 0\n5850 touch 2 98.49895 715.8324 0.0 0.0 0.25882354 0.06666667 65540 0 0\n5873 touch 2 98.12059 714.8264 0.0 0.0 0.2627451 0.06666667 65540 0 0\n5908 touch 2 97.61611 713.569 0.0 0.0 0.27058825 0.06666667 65540 0 0\n5931 touch 2 97.74223 712.31146 0.0 0.0 0.27058825 0.06666667 65540 0 0\n5989 touch 2 99.12954 719.22754 0.0 0.0 0.27058825 0.06666667 65540 0 0\n6001 touch 1 99.12954 719.22754 0.0 0.0 0.27058825 0.06666667 65540 0 0\n7298 touch 0 109.21906 713.569 0.0 0.0 0.23529412 0.06666667 65540 0 0\n7327 touch 2 109.471306 713.94617 0.0 0.0 0.23529412 0.06666667 65540 0 0\n7351 touch 2 113.25487 722.62274 0.0 0.0 0.23529412 0.06666667 65540 0 0\n7363 touch 1 113.25487 722.62274 0.0 0.0 0.23529412 0.06666667 65540 0 0\n10848 screen\n11942 touch 0 244.41864 261.75797 0.0 0.0 0.23529412 0.06666667 65540 0 0\n11963 touch 2 244.29254 261.25494 0.0 0.0 0.2627451 0.06666667 65540 0 0\n12009 touch 2 244.54475 260.12323 0.0 0.0 0.28235295 0.06666667 65540 0 0\n12102 touch 2 244.1664 261.6322 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12125 touch 2 243.66194 263.7699 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12206 touch 2 243.4097 280.24283 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12218 touch 2 243.28355 283.13504 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12241 touch 2 242.52686 288.16495 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12265 touch 2 242.1485 292.8176 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12299 touch 2 241.64401 298.60196 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12325 touch 2 240.63506 302.50015 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12358 touch 2 238.99553 307.9073 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12381 touch 2 237.98657 311.80548 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12418 touch 2 236.59926 319.22458 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12438 touch 2 235.4642 324.50595 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12462 touch 2 234.70747 331.79935 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12497 touch 2 233.44629 345.6316 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12521 touch 2 232.56346 353.6794 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12557 touch 2 231.42838 363.362 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12579 touch 2 230.54555 369.0206 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12602 touch 2 229.6627 375.43375 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12638 touch 2 229.28436 390.39767 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12661 touch 2 228.65376 402.97244 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12684 touch 2 227.77094 412.7808 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12718 touch 2 226.38362 423.59506 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12741 touch 2 226.00526 432.14587 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12776 touch 2 225.37466 451.00806 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12800 touch 2 225.12244 461.1936 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12837 touch 2 224.61797 471.0019 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12858 touch 2 224.23961 476.66052 0.0 0.0 0.29803923 0.06666667 65540 0 0\n12882 touch 2 224.49184 487.85205 0.0 0.0 0.1882353 0.06666667 65540 0 0\n12894 touch 2 224.11348 501.18134 0.0 0.0 0.09019608 0.06666667 65540 0 0\n12906 touch 1 224.11348 501.18134 0.0 0.0 0.09019608 0.06666667 65540 0 0\n13908 touch 0 401.3107 705.8983 0.0 0.0 0.24313726 0.06666667 65540 0 0\n13928 touch 2 400.30176 706.2756 0.0 0.0 0.24705882 0.06666667 65540 0 0\n14020 touch 2 413.92258 714.7006 0.0 0.0 0.05882353 0.2 65540 0 0\n14032 touch 1 413.92258 714.7006 0.0 0.0 0.05882353 0.2 65540 0 0\n14971 touch 0 404.84204 719.35333 0.0 0.0 0.22745098 0.06666667 65540 0 0\n14992 touch 2 405.59875 720.1078 0.0 0.0 0.23137255 0.06666667 65540 0 0\n15050 touch 2 408.7517 724.7605 0.0 0.0 0.23137255 0.06666667 65540 0 0\n15063 touch 1 408.7517 724.7605 0.0 0.0 0.23137255 0.06666667 65540 0 0\n16106 touch 0 416.82333 719.60486 0.0 0.0 0.1882353 0.06666667 65540 0 0\n16115 touch 2 416.0666 718.4731 0.0 0.0 0.20784314 0.06666667 65540 0 0\n16231 touch 2 415.9405 717.4671 0.0 0.0 0.21568628 0.06666667 65540 0 0\n16289 touch 2 418.08453 718.0959 0.0 0.0 0.21568628 0.06666667 65540 0 0\n16313 touch 1 418.08453 718.0959 0.0 0.0 0.21568628 0.06666667 65540 0 0\n16864 touch 0 397.9055 703.88635 0.0 0.0 0.25490198 0.06666667 65540 0 0\n16873 touch 2 398.28384 703.88635 0.0 0.0 0.25882354 0.06666667 65540 0 0\n16911 touch 2 398.28384 705.14386 0.0 0.0 0.2627451 0.06666667 65540 0 0\n16994 touch 2 398.53607 706.14984 0.0 0.0 0.2627451 0.06666667 65540 0 0\n17017 touch 2 401.18457 708.66473 0.0 0.0 0.2627451 0.06666667 65540 0 0\n17028 touch 2 403.70694 710.42523 0.0 0.0 0.2627451 0.06666667 65540 0 0\n17040 touch 1 403.70694 710.42523 0.0 0.0 0.2627451 0.06666667 65540 0 0\n21356 screen\n23145 touch 0 219.44708 180.52502 0.0 0.0 0.24705882 0.06666667 65540 0 0\n23211 touch 2 220.07767 181.15378 0.0 0.0 0.25490198 0.06666667 65540 0 0\n23223 touch 2 218.81648 183.79446 0.0 0.0 0.105882354 0.06666667 65540 0 0\n23235 touch 1 218.81648 183.79446 0.0 0.0 0.105882354 0.06666667 65540 0 0\n24433 touch 0 63.81622 52.38826 0.0 0.0 0.25882354 0.06666667 65540 0 0\n24457 touch 2 63.437862 52.136765 0.0 0.0 0.2627451 0.06666667 65540 0 0\n24511 touch 2 64.82517 50.6278 0.0 0.0 0.27058825 0.06666667 65540 0 0\n24557 touch 1 64.69905 50.502045 0.0 0.0 0.0627451 0.06666667 65540 0 0\n27380 screen\n' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, skipLines = 0): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop((0, skipLines, width - 1, height - 1)) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | skipLines = reply['skipLines'] 210 | captureScreen(localFileName, skipLines) 211 | sendIntent(_INTENT_SCREEN_DONE) 212 | 213 | elif reply['type'] == type: 214 | proc.kill() 215 | clearLogcat() 216 | return reply 217 | 218 | def printUsage(): 219 | app = os.path.basename(sys.argv[0]) 220 | print "Usage: ", app, "\t\t- assume one attached device only" 221 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 222 | print " ", app, _OPTION_HELP, "\t\t- print this help" 223 | 224 | def readData(socket, max = 4096): 225 | return socket.recv(max) 226 | 227 | def readOkay(socket): 228 | data = socket.recv(4) 229 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 230 | 231 | def sendData(socket, str): 232 | return socket.sendall('%04X%s' % (len(str), str)) 233 | 234 | def execute(cmd): 235 | fullCmd = 'adb ' 236 | if targetDevice(): 237 | fullCmd += '-s ' + targetDevice() + ' ' 238 | fullCmd += cmd 239 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 240 | proc.stdin.close() 241 | proc.wait() 242 | 243 | def startAdbServer(): 244 | execute('start-server') 245 | 246 | def query(cmd): 247 | fullCmd = 'adb ' 248 | if targetDevice(): 249 | fullCmd += '-s ' + targetDevice() + ' ' 250 | fullCmd += cmd 251 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 252 | output = proc.stdout.read() 253 | proc.stdin.close() 254 | proc.wait() 255 | return output 256 | 257 | def devices(): 258 | sock = startConnection(_ADB_PORT) 259 | sendData(sock, 'host:devices') 260 | if readOkay(sock): 261 | readData(sock, 4) # payload size in hex 262 | data = readData(sock) 263 | reply = "" 264 | while len(data): 265 | reply += data 266 | data = readData(sock) 267 | sock.close() 268 | devices = re.sub('List of devices attached\s+', '', reply) 269 | devices = devices.splitlines() 270 | list = [] 271 | for elem in devices: 272 | if elem.find('device') != -1: 273 | list.append(re.sub(r'\s*device', '', elem)) 274 | return list 275 | else: # adb server not running 276 | sock.close() 277 | return None 278 | 279 | def isAvailable(): 280 | return query('version').startswith('Android Debug Bridge') 281 | 282 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 283 | clearLogcat() 284 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 285 | if data: 286 | cmd += " -d '" + data + "'" 287 | execute(cmd) 288 | 289 | def pull(remote, local): 290 | execute('pull ' + remote + ' ' + local) 291 | 292 | def push(local, remote): 293 | execute('push ' + local + ' ' + remote) 294 | 295 | def runTest(): 296 | def checkError(r): 297 | error = r['error'] 298 | if error: 299 | setExitCode(ExitCode.Aborted) 300 | setError(error) 301 | raise Exception() 302 | 303 | print "Launching remote application..." 304 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 305 | reply = waitForReply(_REPLY_READY) 306 | checkError(reply) 307 | 308 | print "Sending playback events..." 309 | sendIntent(_INTENT_PLAY) 310 | reply = waitForReply(_REPLY_EVENTS_PATH) 311 | file = tempfile.NamedTemporaryFile() 312 | file.write(_g_events) 313 | file.flush() 314 | 315 | push(file.name, reply["value"]) 316 | file.close() 317 | sendIntent(_INTENT_PUSH_DONE) 318 | 319 | print "Playing test..." 320 | reply = waitForReply(_REPLY_DONE) 321 | checkError(reply) 322 | 323 | prefix = reply['filesPath'] 324 | consoleLogFile = reply['consoleLogFile'] 325 | 326 | print "Fetching results..." 327 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 328 | 329 | print "Done." 330 | 331 | def main(): 332 | args = sys.argv[1:] 333 | 334 | if _OPTION_HELP in args: 335 | printUsage() 336 | return ExitCode.Help 337 | 338 | if not isAvailable(): 339 | print "'adb' not found, please add its location to $PATH." 340 | return ExitCode.AdbNotFound 341 | 342 | startAdbServer() 343 | deviceList = devices() 344 | 345 | if len(deviceList) == 0: 346 | print "No attached devices." 347 | return ExitCode.NoDevices 348 | 349 | if _OPTION_DEVICE in args: 350 | try: 351 | serial = args[args.index(_OPTION_DEVICE) + 1] 352 | except IndexError: 353 | print "Must specify a device serial number." 354 | return ExitCode.WrongUsage 355 | if serial in deviceList: 356 | setTargetDevice(serial) 357 | else: 358 | print "Device " + serial + " not found." 359 | return ExitCode.UnknownDevice 360 | else: 361 | if len(deviceList) > 1: 362 | print "Multiple devices attached, one must be specified." 363 | return ExitCode.MultipleDevices 364 | 365 | print "EventRecorder - Remote Automated Web Application Testing for Android." 366 | if not targetDevice(): 367 | setTargetDevice(deviceList[0]) 368 | 369 | print "Target device is " + targetDevice() + "." 370 | 371 | try: 372 | runTest() 373 | except Exception as e: 374 | print e 375 | code = exitCode() 376 | if code == ExitCode.Normal: 377 | print "Exiting..." 378 | elif code == ExitCode.DeviceDisconnected: 379 | print "Device disconnected." 380 | elif code == ExitCode.Aborted: 381 | print _g_state['error'] 382 | return code 383 | 384 | if __name__ == "__main__": 385 | sys.exit(main()) 386 | -------------------------------------------------------------------------------- /eventrecorder/src/examples/tests/tabs2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | _g_isPilAvailable = False 4 | try: 5 | from PIL import Image 6 | _g_isPilAvailable = True 7 | except: 8 | pass 9 | 10 | from subprocess import Popen, PIPE, STDOUT 11 | import base64 12 | import math 13 | import os 14 | import re 15 | import socket 16 | import struct 17 | import sys 18 | import tempfile 19 | import thread 20 | import time 21 | 22 | _OPTION_DEVICE = "-s" 23 | _OPTION_HELP = "-h" 24 | 25 | _TARGET_PACKAGE = 'com.sencha.eventrecorder' 26 | _TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App' 27 | _STANDARD_PACKAGE = 'android.intent.action' 28 | 29 | _ADB_PORT = 5037 30 | _LOG_FILTER = 'EventRecorder' 31 | 32 | _INTENT_PLAY = "PLAY" 33 | _INTENT_PUSH_DONE = "PUSH_DONE" 34 | _INTENT_SCREEN_DONE = "SCREEN_DONE" 35 | _INTENT_VIEW = "VIEW" 36 | 37 | _REPLY_DONE = 'done' 38 | _REPLY_READY = 'ready' 39 | _REPLY_SCREEN = 'screen' 40 | _REPLY_EVENTS_PATH = 'eventsFilePath' 41 | 42 | _CONSOLE_LOG_FILE_NAME = "console.log" 43 | _SCREEN_CAPTURE_PREFIX = "screen" 44 | _WINDOW_CAPTURE_PREFIX = "window" 45 | 46 | class ExitCode: 47 | Help = -10 48 | Normal = 0 49 | AdbNotFound = 5 50 | NoDevices = 15 51 | DeviceDisconnected = 25 52 | MultipleDevices = 35 53 | Aborted = 45 54 | WrongUsage = 55 55 | UnknownDevice = 65 56 | 57 | _g_state = { 58 | 'exitCode': ExitCode.Normal, 59 | 'error': '', 60 | 'screenCaptureCount': 0, 61 | 'targetDevice': '' 62 | } 63 | 64 | _g_events = '0 url http://dev.sencha.com/deploy/touch/examples/tabs2/\n0 pause\n4100 screen\n5627 touch 0 145.30334 664.0364 0.0 0.0 0.047058824 0.05 65538 0 0\n5665 touch 1 145.30334 664.0364 0.0 0.0 0.047058824 0.05 65538 0 0\n8618 screen\n9556 touch 0 269.0617 676.0 0.0 0.0 0.11764706 0.15 65538 0 0\n9585 touch 2 269.0617 676.0 0.0 0.0 0.16862746 0.2 65538 0 0\n9739 touch 1 269.0617 676.0 0.0 0.0 0.16862746 0.2 65538 0 0\n12111 screen\n13438 touch 0 350.23117 681.12726 0.0 0.0 0.10980392 0.1 65538 0 0\n13533 touch 2 350.23117 681.12726 0.0 0.0 0.15686275 0.2 65538 0 0\n13656 touch 1 350.23117 681.12726 0.0 0.0 0.08627451 0.1 65538 0 0\n17080 screen\n18268 touch 0 436.4111 673.43634 0.0 0.0 0.14117648 0.15 65538 0 0\n18296 touch 2 436.4111 673.43634 0.0 0.0 0.18431373 0.2 65538 0 0\n18434 touch 1 436.4111 673.43634 0.0 0.0 0.11372549 0.15 65538 0 0\n20971 screen\n22559 touch 0 62.630756 671.72723 0.0 0.0 0.19215687 0.2 65538 0 0\n22652 touch 2 62.630756 671.72723 0.0 0.0 0.09019608 0.15 65538 0 0\n22696 touch 1 62.630756 671.72723 0.0 0.0 0.09019608 0.15 65538 0 0\n' 65 | 66 | def exitCode(): 67 | return _g_state['exitCode'] 68 | 69 | def setExitCode(err): 70 | global _g_state 71 | _g_state['exitCode'] = err 72 | 73 | def error(): 74 | return _g_state['error'] 75 | 76 | def setError(err): 77 | global _g_state 78 | _g_state['error'] = err 79 | 80 | def targetDevice(): 81 | return _g_state['targetDevice'] 82 | 83 | def setTargetDevice(id): 84 | global _g_state 85 | _g_state['targetDevice'] = id 86 | 87 | def startConnection(port): 88 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 89 | try: 90 | sock.connect(('127.0.0.1', port)) 91 | return sock 92 | except Exception as e: 93 | setError('Unable to connect to port %d: %s' % (port, e)) 94 | 95 | def clearLogcat(): 96 | cmd = ' logcat -c ' 97 | fullCmd = 'adb ' 98 | if targetDevice(): 99 | fullCmd += '-s ' + targetDevice() + ' ' 100 | fullCmd += cmd 101 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 102 | time.sleep(1) 103 | proc.kill() 104 | 105 | def framebuffer(): 106 | def headerMap(ints): 107 | if len(ints) == 12: 108 | return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3], 109 | 'red': {'offset': ints[4], 'length': ints[5]}, 110 | 'blue': {'offset': ints[6], 'length': ints[7]}, 111 | 'green': {'offset': ints[8], 'length': ints[9]}, 112 | 'alpha': {'offset': ints[10], 'length': ints[11]}} 113 | else: 114 | return {'size': ints[0], 'width': ints[1], 'height': ints[2]} 115 | 116 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 117 | sock.connect(('127.0.0.1', _ADB_PORT)) 118 | sendData(sock, 'host:transport:' + targetDevice()) 119 | ok = readOkay(sock) 120 | if not ok: 121 | return None, None 122 | sendData(sock, 'framebuffer:') 123 | if readOkay(sock): 124 | version = struct.unpack('@I', readData(sock, 4))[0] # ntohl 125 | if version == 16: # compatibility mode 126 | headerFields = 3 # size, width, height 127 | else: 128 | headerFields = 12 # bpp, size, width, height, 4*(offset, length) 129 | header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4))) 130 | sendData(sock, '\x00') 131 | data = readData(sock) 132 | result = "" 133 | while len(data): 134 | result += data 135 | data = readData(sock) 136 | sock.close() 137 | return header, result # pass size returned in header 138 | else: 139 | sock.close() 140 | return None, None 141 | 142 | def captureScreen(localFileName, skipLines = 0): 143 | header, data = framebuffer() 144 | width = header['width'] 145 | height = header['height'] 146 | dimensions = (width, height) 147 | if header['bpp'] == 32: 148 | components = {header['red']['offset']: 'R', 149 | header['green']['offset']: 'G', 150 | header['blue']['offset']: 'B'} 151 | alpha = header['alpha']['length'] != 0 152 | if alpha: 153 | components[header['alpha']['offset']] = 'A' 154 | format = '' + components[0] + components[8] + components[16] 155 | if alpha: 156 | format += components[24] 157 | image = Image.fromstring('RGBA', dimensions, data, 'raw', format) 158 | else: 159 | image = Image.fromstring('RGBA', dimensions, data) 160 | r, g, b, a = image.split() 161 | image = Image.merge('RGB', (r, g, b)) 162 | else: # assuming BGR565 163 | image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16') 164 | image = image.crop((0, skipLines, width - 1, height - 1)) 165 | image.save(localFileName, optimize=1) 166 | 167 | def waitForReply(type): 168 | cmd = ' logcat ' + _LOG_FILTER + ':V *:S' 169 | fullCmd = 'adb ' 170 | if targetDevice(): 171 | fullCmd += '-s ' + targetDevice() + ' ' 172 | fullCmd += cmd 173 | proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT) 174 | 175 | while True: 176 | line = proc.stdout.readline() 177 | 178 | if re.match(r'^[A-Z]/' + _LOG_FILTER, line): 179 | line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line) 180 | line = re.sub(r'Console: ', '', line) 181 | line = re.sub(r':(\d)+(\b)*', '', line) 182 | line = re.sub(r'\r\n', '', line) 183 | 184 | if (line.startswith("#")): 185 | print line 186 | continue 187 | 188 | try: 189 | reply = eval(line) 190 | except Exception as e: 191 | setExitCode(ExitCode.Aborted) 192 | setError('Error in protocol: unrecognized message "' + line + '"') 193 | raise e 194 | 195 | error = reply['error'] 196 | if error: 197 | setExitCode(ExitCode.Aborted) 198 | setError(error) 199 | raise Exception() 200 | 201 | if reply['type'] == _REPLY_SCREEN: 202 | if not _g_isPilAvailable: 203 | setExitCode(ExitCode.Aborted) 204 | setError('Screen capture requested but Python Imaging Library (PIL) not found.') 205 | raise Exception() 206 | 207 | _g_state['screenCaptureCount'] += 1 208 | localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png' 209 | skipLines = reply['skipLines'] 210 | captureScreen(localFileName, skipLines) 211 | sendIntent(_INTENT_SCREEN_DONE) 212 | 213 | elif reply['type'] == type: 214 | proc.kill() 215 | clearLogcat() 216 | return reply 217 | 218 | def printUsage(): 219 | app = os.path.basename(sys.argv[0]) 220 | print "Usage: ", app, "\t\t- assume one attached device only" 221 | print " ", app, _OPTION_DEVICE, "\t- connect to device with serial number " 222 | print " ", app, _OPTION_HELP, "\t\t- print this help" 223 | 224 | def readData(socket, max = 4096): 225 | return socket.recv(max) 226 | 227 | def readOkay(socket): 228 | data = socket.recv(4) 229 | return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y' 230 | 231 | def sendData(socket, str): 232 | return socket.sendall('%04X%s' % (len(str), str)) 233 | 234 | def execute(cmd): 235 | fullCmd = 'adb ' 236 | if targetDevice(): 237 | fullCmd += '-s ' + targetDevice() + ' ' 238 | fullCmd += cmd 239 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 240 | proc.stdin.close() 241 | proc.wait() 242 | 243 | def startAdbServer(): 244 | execute('start-server') 245 | 246 | def query(cmd): 247 | fullCmd = 'adb ' 248 | if targetDevice(): 249 | fullCmd += '-s ' + targetDevice() + ' ' 250 | fullCmd += cmd 251 | proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 252 | output = proc.stdout.read() 253 | proc.stdin.close() 254 | proc.wait() 255 | return output 256 | 257 | def devices(): 258 | sock = startConnection(_ADB_PORT) 259 | sendData(sock, 'host:devices') 260 | if readOkay(sock): 261 | readData(sock, 4) # payload size in hex 262 | data = readData(sock) 263 | reply = "" 264 | while len(data): 265 | reply += data 266 | data = readData(sock) 267 | sock.close() 268 | devices = re.sub('List of devices attached\s+', '', reply) 269 | devices = devices.splitlines() 270 | list = [] 271 | for elem in devices: 272 | if elem.find('device') != -1: 273 | list.append(re.sub(r'\s*device', '', elem)) 274 | return list 275 | else: # adb server not running 276 | sock.close() 277 | return None 278 | 279 | def isAvailable(): 280 | return query('version').startswith('Android Debug Bridge') 281 | 282 | def sendIntent(intent, package=_TARGET_PACKAGE, data=''): 283 | clearLogcat() 284 | cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY 285 | if data: 286 | cmd += " -d '" + data + "'" 287 | execute(cmd) 288 | 289 | def pull(remote, local): 290 | execute('pull ' + remote + ' ' + local) 291 | 292 | def push(local, remote): 293 | execute('push ' + local + ' ' + remote) 294 | 295 | def runTest(): 296 | def checkError(r): 297 | error = r['error'] 298 | if error: 299 | setExitCode(ExitCode.Aborted) 300 | setError(error) 301 | raise Exception() 302 | 303 | print "Launching remote application..." 304 | sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE) 305 | reply = waitForReply(_REPLY_READY) 306 | checkError(reply) 307 | 308 | print "Sending playback events..." 309 | sendIntent(_INTENT_PLAY) 310 | reply = waitForReply(_REPLY_EVENTS_PATH) 311 | file = tempfile.NamedTemporaryFile() 312 | file.write(_g_events) 313 | file.flush() 314 | 315 | push(file.name, reply["value"]) 316 | file.close() 317 | sendIntent(_INTENT_PUSH_DONE) 318 | 319 | print "Playing test..." 320 | reply = waitForReply(_REPLY_DONE) 321 | checkError(reply) 322 | 323 | prefix = reply['filesPath'] 324 | consoleLogFile = reply['consoleLogFile'] 325 | 326 | print "Fetching results..." 327 | pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME) 328 | 329 | print "Done." 330 | 331 | def main(): 332 | args = sys.argv[1:] 333 | 334 | if _OPTION_HELP in args: 335 | printUsage() 336 | return ExitCode.Help 337 | 338 | if not isAvailable(): 339 | print "'adb' not found, please add its location to $PATH." 340 | return ExitCode.AdbNotFound 341 | 342 | startAdbServer() 343 | deviceList = devices() 344 | 345 | if len(deviceList) == 0: 346 | print "No attached devices." 347 | return ExitCode.NoDevices 348 | 349 | if _OPTION_DEVICE in args: 350 | try: 351 | serial = args[args.index(_OPTION_DEVICE) + 1] 352 | except IndexError: 353 | print "Must specify a device serial number." 354 | return ExitCode.WrongUsage 355 | if serial in deviceList: 356 | setTargetDevice(serial) 357 | else: 358 | print "Device " + serial + " not found." 359 | return ExitCode.UnknownDevice 360 | else: 361 | if len(deviceList) > 1: 362 | print "Multiple devices attached, one must be specified." 363 | return ExitCode.MultipleDevices 364 | 365 | print "EventRecorder - Remote Automated Web Application Testing for Android." 366 | if not targetDevice(): 367 | setTargetDevice(deviceList[0]) 368 | 369 | print "Target device is " + targetDevice() + "." 370 | 371 | try: 372 | runTest() 373 | except Exception as e: 374 | print e 375 | code = exitCode() 376 | if code == ExitCode.Normal: 377 | print "Exiting..." 378 | elif code == ExitCode.DeviceDisconnected: 379 | print "Device disconnected." 380 | elif code == ExitCode.Aborted: 381 | print _g_state['error'] 382 | return code 383 | 384 | if __name__ == "__main__": 385 | sys.exit(main()) 386 | -------------------------------------------------------------------------------- /eventrecorder/src/shell/imagediff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2011 Sencha Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import os 26 | import sys 27 | 28 | try: 29 | from PIL import Image, ImageChops, ImageOps 30 | except: 31 | print 'Error: the Python Imaging Library is required (see http://www.pythonware.com/products/pil/).' 32 | sys.exit(1) 33 | 34 | def printUsage(): 35 | app = os.path.basename(sys.argv[0]) 36 | print "Usage:", app, " " 37 | 38 | def main(): 39 | args = sys.argv[1:] 40 | numberOfArgs = len(args) 41 | if numberOfArgs != 3: 42 | print 'Error: exactly 3 arguments required.' 43 | printUsage() 44 | return 1 45 | 46 | try: 47 | image1 = Image.open(args[0]) 48 | image1.load() 49 | except IOError as e: 50 | fileName = "'" + args[0] + "'" 51 | if len(e.args) == 2: 52 | print 'Error:', e.args[1], fileName 53 | return 2 54 | print 'Error:', e, fileName 55 | return 3 56 | 57 | try: 58 | image2 = Image.open(args[1]) 59 | image2.load() 60 | except IOError as e: 61 | fileName = "'" + args[1] + "'" 62 | if len(e.args) == 2: 63 | print 'Error:', e.args[1], fileName 64 | return 2 65 | print 'Error:', e, fileName 66 | return 3 67 | 68 | difference = ImageChops.difference(image1, image2) 69 | if difference.getbbox() is None: 70 | return 0; 71 | difference = difference.convert('RGB') 72 | difference = ImageOps.grayscale(difference) 73 | difference = ImageOps.invert(difference) 74 | try: 75 | difference.save(args[2], optimize='1') 76 | except KeyError: 77 | print 'Error: a valid output image file extension must be provided' 78 | 79 | if __name__ == "__main__": 80 | sys.exit(main()) 81 | -------------------------------------------------------------------------------- /eventrecorder/src/shell/inline-apk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | import os 5 | 6 | if __name__ == "__main__": 7 | fileName = "../android/bin/EventRecorder.apk" 8 | file = open(fileName, "rb") 9 | fileContent = file.read() 10 | file.close() 11 | apk = base64.b64encode(fileContent) 12 | 13 | fileName = "recorder.py" 14 | file = open(fileName, "rb") 15 | fileLines = file.readlines() 16 | file.close() 17 | 18 | file = open(fileName, "wb") 19 | for i in range(len(fileLines)): 20 | if fileLines[i].startswith("_g_base64Apk = "): 21 | fileLines[i] = '_g_base64Apk = b"' + apk + '"\n' 22 | break 23 | file.writelines(fileLines) 24 | file.close() 25 | -------------------------------------------------------------------------------- /remotejs/README.md: -------------------------------------------------------------------------------- 1 | RemoteJS 2 | === 3 | 4 | What it is 5 | --- 6 | _RemoteJS_ is an application that works as a remote [JavaScript](http://en.wikipedia.org/wiki/JavaScript) console. Think of [Web Inspector](http://trac.webkit.org/wiki/WebInspector) running on your workstation, debugging code running on your mobile phone or tablet device. 7 | There are two versions of the application. One has a GUI and is meant to be run on a desktop, the other is a [Python](http://www.python.org/) application that runs on a shell. The latter is useful for quick debugging on a text environment, but is particularly designed for scripting and test automation. 8 | 9 | Supported platforms 10 | --- 11 | The applications run on Windows, Mac OS X, and Linux, and should run on any platform that is simultaneously supported by the [Android Debug Bridge](http://developer.android.com/guide/developing/tools/adb.html) (adb) and Python (in the non-GUI version). 12 | Currently, it is able to debug code on any devices running [Android](http://www.android.com) ([2.1](http://developer.android.com/sdk/android-2.1.html) or [2.2](http://developer.android.com/sdk/android-2.2.html)). The Android [emulator](http://developer.android.com/guide/developing/tools/emulator.html) is also supported, allowing you to debug your code without a physical device. 13 | 14 | Requirements 15 | --- 16 | The Android [SDK](http://developer.android.com/sdk/) is necessary to run the application. In particular, the adb tool is required to be in your _$PATH_. 17 | To run the shell version, a Python environment is required. 18 | 19 | Features 20 | --- 21 | 22 | When you launch _RemoteJS_ (GUI version), it will try to detect all attached devices. If there is none, it will wait for the first to be connected and make it the target device. If there' more than one device attached, you'll be asked to select one. 23 | 24 | After your target device is finally set, you can simply unplug it and the application will automatically detect the disconnection. Again, if there's only one device left, that will be chosen for you. If there are at least two, then you'll need to select one again, otherwise the console will wait for one to be attached indefinitely. 25 | 26 | After all is set up, most of the times you want to debug a specific web page. In order to load an URL, enter it: 27 | 28 | www.sencha.com 29 | 30 | The page will start being loaded on the device and the message `Opening www.sencha.com` will be shown on the console. Expressions starting with `www.`, `http://`, `https://` or `ftp://` are automatically identified as _URL_'s. 31 | 32 | You can print values with the `console.log` function: 33 | 34 | > for (var x in document) console.log(x) 35 | bgColor 36 | alinkColor 37 | width 38 | ... 39 | 40 | Multiple instructions are also supported: 41 | 42 | > var a = [1,2,3]; console.log(a) 43 | 1,2,3 44 | 45 | Sometimes you might get an error: 46 | 47 | > console.log(documant.title) 48 | ReferenceError: Can't find variable: documant 49 | 50 | To recover previously entered expressions, press `Arrow/Page Up` and you will be able to browse the history. `Arrow/Page Down` works in the usual opposite way, giving you the expressions entered after the current one. The history is stored when you quit the application, which means it will still be available when you start a new session. 51 | 52 | The non-GUI version works in a very similar way. You can type `python remotejs.py -h` to check the usage. 53 | 54 | How it works 55 | --- 56 | When the application is started, it automatically installs an Android package ([apk](http://en.wikipedia.org/wiki/APK_(file_format)) called _RemoteJS_ on your selected device. If a package already exists, it is *uninstalled* first. This ensures total compatibility with the remote console. 57 | 58 | This proxy [activity](http://developer.android.com/guide/topics/fundamentals.html#appcomp) will be the one receiving [intents](http://developer.android.com/guide/topics/fundamentals.html#actcomp) requesting the execution of the _JavaScript_ code. The evaluation results are then [logged](http://developer.android.com/reference/android/util/Log.html) in Android. Meanwhile, our console is already listening and filtering that very same log through _adb_ and [logcat](http://developer.android.com/guide/developing/debug-tasks.html). The processed output is finally presented on the screen. 59 | 60 | Implementation details 61 | --- 62 | _RemoteJS_ is a native application, but in reality there's only a very thin layer of native code. It is only necessary to deal with _adb_ at a system level. This component of the program is written in [C++](http://en.wikipedia.org/wiki/C%2B%2B) and [Qt](http://qt.nokia.com/). In particular, the [QtWebKit](http://doc.qt.nokia.com/qtwebkit.html) module is crucial, since the rest of the application is written with _HTML_, _JavaScript_ and _CSS_. 63 | 64 | The thin system-level functionality is exported from the _Qt_ space to the [web view](http://doc.qt.nokia.com/qwebview.html) through a very simple API. All the logic is then implemented on the _JS_ side, including the entire console window, turning the native nature of the application into hybrid. 65 | -------------------------------------------------------------------------------- /remotejs/src/android/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /remotejs/src/android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | RemoteJS 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /remotejs/src/android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /remotejs/src/android/README.md: -------------------------------------------------------------------------------- 1 | RemoteJS Device Tool 2 | === 3 | 4 | What it is 5 | --- 6 | 7 | It's a tool that needs to be installed on the Android device or emulator for the remote JavaScript console to work. 8 | You don't need to manually install it, the applications will do it for you. 9 | 10 | How to build 11 | --- 12 | 13 | You can import the folder as a new project in Eclipse and [build the project](http://developer.android.com/guide/developing/eclipse-adt.html). 14 | If you prefer a lighter environment, you can generate an [Ant](http://ant.apache.org/) _build.xml_ file by running _./generate.ant.sh_ or simply executing `android update project -p .`. Then type `ant debug` and the APK will be generated in the _bin_ folder. 15 | -------------------------------------------------------------------------------- /remotejs/src/android/bin/RemoteJS.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/remotejs/src/android/bin/RemoteJS.apk -------------------------------------------------------------------------------- /remotejs/src/android/default.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "build.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-7 12 | -------------------------------------------------------------------------------- /remotejs/src/android/generate.ant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ ! -f "build.xml" -o ! -f "local.properties" ]; then 3 | android update project -p . 4 | fi 5 | -------------------------------------------------------------------------------- /remotejs/src/android/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/remotejs/src/android/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /remotejs/src/android/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | RemoteJS 4 | 5 | -------------------------------------------------------------------------------- /remotejs/src/android/src/com/sencha/remotejs/Base64.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.remotejs; 24 | 25 | import java.text.ParseException; 26 | 27 | public class Base64 { 28 | public static String encode(String input) { 29 | return ""; 30 | } 31 | 32 | public static String decode(String input) throws ParseException { 33 | int len = ((input.length() + 3) / 4) * 3; 34 | byte[] result = new byte[len]; 35 | int pos = -1; 36 | 37 | char c; 38 | int temp = 0; 39 | for (int i = 0; i < input.length(); ++i) { 40 | c = input.charAt(i); 41 | 42 | temp <<= 6; 43 | if (c >= 'A' && c <= 'Z') 44 | temp |= (byte)(c - 'A'); 45 | else if (c >= 'a' && c <= 'z') 46 | temp |= (byte)(c - 'a' + 26); 47 | else if (c >= '0' && c <= '9') 48 | temp |= (byte)(c - '0' + 52); 49 | else if (c == '+') 50 | temp |= 62; 51 | else if (c == '/') 52 | temp |= 63; 53 | else if (c == '=') { 54 | switch (input.length() - i) { 55 | case 1: 56 | result[++pos] = (byte)((temp >> 16) & 0xff); 57 | result[++pos] = (byte)((temp >> 8) & 0xff); 58 | return new String(result, 0, len - 1); 59 | case 2: 60 | result[++pos] = (byte)((temp >> 10) & 0xff); 61 | return new String(result, 0, len - 2); 62 | default: 63 | throw new ParseException("Invalid number of pad characters", i); 64 | } 65 | } else { 66 | throw new ParseException("Invalid character: " + c, i); 67 | } 68 | if ((i + 1) % 4 == 0) { 69 | result[++pos] = (byte)((temp >> 16) & 0xff); 70 | result[++pos] = (byte)((temp >> 8) & 0xff); 71 | result[++pos] = (byte)(temp & 0xff); 72 | } 73 | } 74 | return new String(result); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /remotejs/src/android/src/com/sencha/remotejs/RemoteJS.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package com.sencha.remotejs; 24 | 25 | import android.app.Activity; 26 | import android.content.Intent; 27 | import android.graphics.*; 28 | import android.os.Bundle; 29 | import android.util.Log; 30 | import android.webkit.*; 31 | import java.io.*; 32 | import java.text.ParseException; 33 | 34 | public class RemoteJS extends Activity { 35 | 36 | static final String LOGTAG = "RemoteJS"; 37 | 38 | private WebView mWebView; 39 | 40 | public void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | 43 | mWebView = new WebView(this); 44 | mWebView.getSettings().setJavaScriptEnabled(true); 45 | mWebView.getSettings().setDomStorageEnabled(true); 46 | mWebView.getSettings().setUseWideViewPort(true); 47 | mWebView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); 48 | 49 | mWebView.setWebChromeClient(new WebChromeClient() { 50 | public void onConsoleMessage(String message, int lineNumber, String sourceID) { 51 | Log.d(LOGTAG, message); 52 | } 53 | 54 | public void onProgressChanged(WebView view, int percent) { 55 | if (percent < 100) { 56 | RemoteJS.this.setTitle("RemoteJS [" + percent + "%]"); 57 | } else { 58 | RemoteJS.this.setTitle("RemoteJS [Loaded]"); 59 | } 60 | } 61 | }); 62 | mWebView.setWebViewClient(new WebViewClient() { 63 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 64 | view.loadUrl(url); 65 | return true; 66 | } 67 | }); 68 | 69 | setContentView(mWebView); 70 | 71 | Intent intent = getIntent(); 72 | String base64 = intent.getDataString(); 73 | String address; 74 | if (base64 == null) { 75 | address = "http://www.example.com"; 76 | mWebView.loadUrl(address); 77 | } else { 78 | try { 79 | address = Base64.decode(base64); 80 | mWebView.loadUrl(address); 81 | } catch (ParseException e) { 82 | } 83 | } 84 | } 85 | 86 | static final String ACTION_CAPTURE = "com.sencha.remotejs.ACTION_CAPTURE"; 87 | 88 | protected void onNewIntent(Intent intent) { 89 | if (ACTION_CAPTURE.equals(intent.getAction())) { 90 | Log.i(LOGTAG, "Capture start"); 91 | Picture picture = mWebView.capturePicture(); 92 | Bitmap buffer = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(), Bitmap.Config.RGB_565); 93 | Canvas canvas = new Canvas(buffer); 94 | canvas.translate(-mWebView.getScrollX(), -mWebView.getScrollY()); 95 | canvas.scale(mWebView.getScale(), mWebView.getScale()); 96 | canvas.drawARGB(255, 255, 255, 255); 97 | canvas.drawPicture(picture); 98 | Log.i(LOGTAG, "Capture finished."); 99 | try { 100 | File output = new File(getCacheDir(), "remotejs-capture.png"); 101 | Log.i(LOGTAG, "About to save to " + output.getAbsolutePath()); 102 | output.createNewFile(); 103 | FileOutputStream stream = new FileOutputStream(output); 104 | buffer.compress(Bitmap.CompressFormat.PNG, 100, stream); 105 | stream.close(); 106 | Log.i(LOGTAG, "Capture saved to " + output.getName()); 107 | } catch (Exception e) { 108 | Log.i(LOGTAG, "Capture error: " + e.toString()); 109 | } 110 | } else { 111 | String base64 = intent.getDataString(); 112 | if (base64 != null) { 113 | try { 114 | String address = Base64.decode(base64); 115 | mWebView.loadUrl(address); 116 | } catch (ParseException e) { 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /remotejs/src/desktop/README.md: -------------------------------------------------------------------------------- 1 | RemoteJS desktop application 2 | === 3 | 4 | What it is 5 | --- 6 | 7 | It's the GUI version of RemoteJS. 8 | 9 | How to build 10 | --- 11 | 12 | It requires the [Qt](http://qt.nokia.com/) framework. 13 | After the dependency is met, to build, run: 14 | 15 | qmake 16 | make 17 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2009 Marijn Haverbeke 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any 5 | damages arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any 8 | purpose, including commercial applications, and to alter it and 9 | redistribute it freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must 12 | not claim that you wrote the original software. If you use this 13 | software in a product, an acknowledgment in the product 14 | documentation would be appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must 17 | not be misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. 21 | 22 | Marijn Haverbeke 23 | marijnh at gmail 24 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/css/jscolors.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor: text; 3 | } 4 | 5 | .editbox { 6 | margin: .4em; 7 | padding: 0; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | color: black; 11 | } 12 | 13 | pre.code, .editbox { 14 | color: #666666; 15 | } 16 | 17 | .editbox p { 18 | margin: 0; 19 | } 20 | 21 | span.js-punctuation { 22 | color: #666666; 23 | } 24 | 25 | span.js-operator { 26 | color: #666666; 27 | } 28 | 29 | span.js-keyword { 30 | color: #770088; 31 | } 32 | 33 | span.js-atom { 34 | color: #228811; 35 | } 36 | 37 | span.js-variable { 38 | color: black; 39 | } 40 | 41 | span.js-variabledef { 42 | color: #0000FF; 43 | } 44 | 45 | span.js-localvariable { 46 | color: #004499; 47 | } 48 | 49 | span.js-property { 50 | color: black; 51 | } 52 | 53 | span.js-comment { 54 | color: #AA7700; 55 | } 56 | 57 | span.js-string { 58 | color: #AA2222; 59 | } 60 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/js/highlight.js: -------------------------------------------------------------------------------- 1 | // Minimal framing needed to use CodeMirror-style parsers to highlight 2 | // code. Load this along with tokenize.js, stringstream.js, and your 3 | // parser. Then call highlightText, passing a string as the first 4 | // argument, and as the second argument either a callback function 5 | // that will be called with an array of SPAN nodes for every line in 6 | // the code, or a DOM node to which to append these spans, and 7 | // optionally (not needed if you only loaded one parser) a parser 8 | // object. 9 | 10 | // Stuff from util.js that the parsers are using. 11 | var StopIteration = {toString: function() {return "StopIteration"}}; 12 | 13 | var Editor = {}; 14 | var indentUnit = 2; 15 | 16 | (function(){ 17 | function normaliseString(string) { 18 | var tab = ""; 19 | for (var i = 0; i < indentUnit; i++) tab += " "; 20 | 21 | string = string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n"); 22 | var pos = 0, parts = [], lines = string.split("\n"); 23 | for (var line = 0; line < lines.length; line++) { 24 | if (line != 0) parts.push("\n"); 25 | parts.push(lines[line]); 26 | } 27 | 28 | return { 29 | next: function() { 30 | if (pos < parts.length) return parts[pos++]; 31 | else throw StopIteration; 32 | } 33 | }; 34 | } 35 | 36 | window.highlightText = function(string, callback, parser) { 37 | parser = (parser || Editor.Parser).make(stringStream(normaliseString(string))); 38 | var line = []; 39 | if (callback.nodeType == 1) { 40 | var node = callback; 41 | callback = function(line) { 42 | for (var i = 0; i < line.length; i++) 43 | node.appendChild(line[i]); 44 | node.appendChild(document.createElement("BR")); 45 | }; 46 | } 47 | 48 | try { 49 | while (true) { 50 | var token = parser.next(); 51 | if (token.value == "\n") { 52 | callback(line); 53 | line = []; 54 | } 55 | else { 56 | var span = document.createElement("SPAN"); 57 | span.className = token.style; 58 | span.appendChild(document.createTextNode(token.value)); 59 | line.push(span); 60 | } 61 | } 62 | } 63 | catch (e) { 64 | if (e != StopIteration) throw e; 65 | } 66 | if (line.length) callback(line); 67 | } 68 | })(); 69 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/js/parsejavascript.js: -------------------------------------------------------------------------------- 1 | /* Parse function for JavaScript. Makes use of the tokenizer from 2 | * tokenizejavascript.js. Note that your parsers do not have to be 3 | * this complicated -- if you don't want to recognize local variables, 4 | * in many languages it is enough to just look for braces, semicolons, 5 | * parentheses, etc, and know when you are inside a string or comment. 6 | * 7 | * See manual.html for more info about the parser interface. 8 | */ 9 | 10 | var JSParser = Editor.Parser = (function() { 11 | // Token types that can be considered to be atoms. 12 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true}; 13 | // Setting that can be used to have JSON data indent properly. 14 | var json = false; 15 | // Constructor for the lexical context objects. 16 | function JSLexical(indented, column, type, align, prev, info) { 17 | // indentation at start of this line 18 | this.indented = indented; 19 | // column at which this scope was opened 20 | this.column = column; 21 | // type of scope ('vardef', 'stat' (statement), 'form' (special form), '[', '{', or '(') 22 | this.type = type; 23 | // '[', '{', or '(' blocks that have any text after their opening 24 | // character are said to be 'aligned' -- any lines below are 25 | // indented all the way to the opening character. 26 | if (align != null) 27 | this.align = align; 28 | // Parent scope, if any. 29 | this.prev = prev; 30 | this.info = info; 31 | } 32 | 33 | // My favourite JavaScript indentation rules. 34 | function indentJS(lexical) { 35 | return function(firstChars) { 36 | var firstChar = firstChars && firstChars.charAt(0), type = lexical.type; 37 | var closing = firstChar == type; 38 | if (type == "vardef") 39 | return lexical.indented + 4; 40 | else if (type == "form" && firstChar == "{") 41 | return lexical.indented; 42 | else if (type == "stat" || type == "form") 43 | return lexical.indented + indentUnit; 44 | else if (lexical.info == "switch" && !closing) 45 | return lexical.indented + (/^(?:case|default)\b/.test(firstChars) ? indentUnit : 2 * indentUnit); 46 | else if (lexical.align) 47 | return lexical.column - (closing ? 1 : 0); 48 | else 49 | return lexical.indented + (closing ? 0 : indentUnit); 50 | }; 51 | } 52 | 53 | // The parser-iterator-producing function itself. 54 | function parseJS(input, basecolumn) { 55 | // Wrap the input in a token stream 56 | var tokens = tokenizeJavaScript(input); 57 | // The parser state. cc is a stack of actions that have to be 58 | // performed to finish the current statement. For example we might 59 | // know that we still need to find a closing parenthesis and a 60 | // semicolon. Actions at the end of the stack go first. It is 61 | // initialized with an infinitely looping action that consumes 62 | // whole statements. 63 | var cc = [json ? expressions : statements]; 64 | // Context contains information about the current local scope, the 65 | // variables defined in that, and the scopes above it. 66 | var context = null; 67 | // The lexical scope, used mostly for indentation. 68 | var lexical = new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false); 69 | // Current column, and the indentation at the start of the current 70 | // line. Used to create lexical scope objects. 71 | var column = 0; 72 | var indented = 0; 73 | // Variables which are used by the mark, cont, and pass functions 74 | // below to communicate with the driver loop in the 'next' 75 | // function. 76 | var consume, marked; 77 | 78 | // The iterator object. 79 | var parser = {next: next, copy: copy}; 80 | 81 | function next(){ 82 | // Start by performing any 'lexical' actions (adjusting the 83 | // lexical variable), or the operations below will be working 84 | // with the wrong lexical state. 85 | while(cc[cc.length - 1].lex) 86 | cc.pop()(); 87 | 88 | // Fetch a token. 89 | var token = tokens.next(); 90 | 91 | // Adjust column and indented. 92 | if (token.type == "whitespace" && column == 0) 93 | indented = token.value.length; 94 | column += token.value.length; 95 | if (token.content == "\n"){ 96 | indented = column = 0; 97 | // If the lexical scope's align property is still undefined at 98 | // the end of the line, it is an un-aligned scope. 99 | if (!("align" in lexical)) 100 | lexical.align = false; 101 | // Newline tokens get an indentation function associated with 102 | // them. 103 | token.indentation = indentJS(lexical); 104 | } 105 | // No more processing for meaningless tokens. 106 | if (token.type == "whitespace" || token.type == "comment") 107 | return token; 108 | // When a meaningful token is found and the lexical scope's 109 | // align is undefined, it is an aligned scope. 110 | if (!("align" in lexical)) 111 | lexical.align = true; 112 | 113 | // Execute actions until one 'consumes' the token and we can 114 | // return it. 115 | while(true) { 116 | consume = marked = false; 117 | // Take and execute the topmost action. 118 | cc.pop()(token.type, token.content); 119 | if (consume){ 120 | // Marked is used to change the style of the current token. 121 | if (marked) 122 | token.style = marked; 123 | // Here we differentiate between local and global variables. 124 | else if (token.type == "variable" && inScope(token.content)) 125 | token.style = "js-localvariable"; 126 | return token; 127 | } 128 | } 129 | } 130 | 131 | // This makes a copy of the parser state. It stores all the 132 | // stateful variables in a closure, and returns a function that 133 | // will restore them when called with a new input stream. Note 134 | // that the cc array has to be copied, because it is contantly 135 | // being modified. Lexical objects are not mutated, and context 136 | // objects are not mutated in a harmful way, so they can be shared 137 | // between runs of the parser. 138 | function copy(){ 139 | var _context = context, _lexical = lexical, _cc = cc.concat([]), _tokenState = tokens.state; 140 | 141 | return function copyParser(input){ 142 | context = _context; 143 | lexical = _lexical; 144 | cc = _cc.concat([]); // copies the array 145 | column = indented = 0; 146 | tokens = tokenizeJavaScript(input, _tokenState); 147 | return parser; 148 | }; 149 | } 150 | 151 | // Helper function for pushing a number of actions onto the cc 152 | // stack in reverse order. 153 | function push(fs){ 154 | for (var i = fs.length - 1; i >= 0; i--) 155 | cc.push(fs[i]); 156 | } 157 | // cont and pass are used by the action functions to add other 158 | // actions to the stack. cont will cause the current token to be 159 | // consumed, pass will leave it for the next action. 160 | function cont(){ 161 | push(arguments); 162 | consume = true; 163 | } 164 | function pass(){ 165 | push(arguments); 166 | consume = false; 167 | } 168 | // Used to change the style of the current token. 169 | function mark(style){ 170 | marked = style; 171 | } 172 | 173 | // Push a new scope. Will automatically link the current scope. 174 | function pushcontext(){ 175 | context = {prev: context, vars: {"this": true, "arguments": true}}; 176 | } 177 | // Pop off the current scope. 178 | function popcontext(){ 179 | context = context.prev; 180 | } 181 | // Register a variable in the current scope. 182 | function register(varname){ 183 | if (context){ 184 | mark("js-variabledef"); 185 | context.vars[varname] = true; 186 | } 187 | } 188 | // Check whether a variable is defined in the current scope. 189 | function inScope(varname){ 190 | var cursor = context; 191 | while (cursor) { 192 | if (cursor.vars[varname]) 193 | return true; 194 | cursor = cursor.prev; 195 | } 196 | return false; 197 | } 198 | 199 | // Push a new lexical context of the given type. 200 | function pushlex(type, info) { 201 | var result = function(){ 202 | lexical = new JSLexical(indented, column, type, null, lexical, info) 203 | }; 204 | result.lex = true; 205 | return result; 206 | } 207 | // Pop off the current lexical context. 208 | function poplex(){ 209 | if (lexical.type == ")") 210 | indented = lexical.indented; 211 | lexical = lexical.prev; 212 | } 213 | poplex.lex = true; 214 | // The 'lex' flag on these actions is used by the 'next' function 215 | // to know they can (and have to) be ran before moving on to the 216 | // next token. 217 | 218 | // Creates an action that discards tokens until it finds one of 219 | // the given type. 220 | function expect(wanted){ 221 | return function expecting(type){ 222 | if (type == wanted) cont(); 223 | else if (wanted == ";") pass(); 224 | else cont(arguments.callee); 225 | }; 226 | } 227 | 228 | // Looks for a statement, and then calls itself. 229 | function statements(type){ 230 | return pass(statement, statements); 231 | } 232 | function expressions(type){ 233 | return pass(expression, expressions); 234 | } 235 | // Dispatches various types of statements based on the type of the 236 | // current token. 237 | function statement(type){ 238 | if (type == "var") cont(pushlex("vardef"), vardef1, expect(";"), poplex); 239 | else if (type == "keyword a") cont(pushlex("form"), expression, statement, poplex); 240 | else if (type == "keyword b") cont(pushlex("form"), statement, poplex); 241 | else if (type == "{") cont(pushlex("}"), block, poplex); 242 | else if (type == ";") cont(); 243 | else if (type == "function") cont(functiondef); 244 | else if (type == "for") cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), poplex, statement, poplex); 245 | else if (type == "variable") cont(pushlex("stat"), maybelabel); 246 | else if (type == "switch") cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), block, poplex, poplex); 247 | else if (type == "case") cont(expression, expect(":")); 248 | else if (type == "default") cont(expect(":")); 249 | else if (type == "catch") cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), statement, poplex, popcontext); 250 | else pass(pushlex("stat"), expression, expect(";"), poplex); 251 | } 252 | // Dispatch expression types. 253 | function expression(type){ 254 | if (atomicTypes.hasOwnProperty(type)) cont(maybeoperator); 255 | else if (type == "function") cont(functiondef); 256 | else if (type == "keyword c") cont(expression); 257 | else if (type == "(") cont(pushlex(")"), expression, expect(")"), poplex, maybeoperator); 258 | else if (type == "operator") cont(expression); 259 | else if (type == "[") cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator); 260 | else if (type == "{") cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator); 261 | else cont(); 262 | } 263 | // Called for places where operators, function calls, or 264 | // subscripts are valid. Will skip on to the next action if none 265 | // is found. 266 | function maybeoperator(type, value){ 267 | if (type == "operator" && /\+\+|--/.test(value)) cont(maybeoperator); 268 | else if (type == "operator") cont(expression); 269 | else if (type == ";") pass(); 270 | else if (type == "(") cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator); 271 | else if (type == ".") cont(property, maybeoperator); 272 | else if (type == "[") cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator); 273 | } 274 | // When a statement starts with a variable name, it might be a 275 | // label. If no colon follows, it's a regular statement. 276 | function maybelabel(type){ 277 | if (type == ":") cont(poplex, statement); 278 | else pass(maybeoperator, expect(";"), poplex); 279 | } 280 | // Property names need to have their style adjusted -- the 281 | // tokenizer thinks they are variables. 282 | function property(type){ 283 | if (type == "variable") {mark("js-property"); cont();} 284 | } 285 | // This parses a property and its value in an object literal. 286 | function objprop(type){ 287 | if (type == "variable") mark("js-property"); 288 | if (atomicTypes.hasOwnProperty(type)) cont(expect(":"), expression); 289 | } 290 | // Parses a comma-separated list of the things that are recognized 291 | // by the 'what' argument. 292 | function commasep(what, end){ 293 | function proceed(type) { 294 | if (type == ",") cont(what, proceed); 295 | else if (type == end) cont(); 296 | else cont(expect(end)); 297 | } 298 | return function commaSeparated(type) { 299 | if (type == end) cont(); 300 | else pass(what, proceed); 301 | }; 302 | } 303 | // Look for statements until a closing brace is found. 304 | function block(type){ 305 | if (type == "}") cont(); 306 | else pass(statement, block); 307 | } 308 | // Variable definitions are split into two actions -- 1 looks for 309 | // a name or the end of the definition, 2 looks for an '=' sign or 310 | // a comma. 311 | function vardef1(type, value){ 312 | if (type == "variable"){register(value); cont(vardef2);} 313 | else cont(); 314 | } 315 | function vardef2(type, value){ 316 | if (value == "=") cont(expression, vardef2); 317 | else if (type == ",") cont(vardef1); 318 | } 319 | // For loops. 320 | function forspec1(type){ 321 | if (type == "var") cont(vardef1, forspec2); 322 | else if (type == ";") pass(forspec2); 323 | else if (type == "variable") cont(formaybein); 324 | else pass(forspec2); 325 | } 326 | function formaybein(type, value){ 327 | if (value == "in") cont(expression); 328 | else cont(maybeoperator, forspec2); 329 | } 330 | function forspec2(type, value){ 331 | if (type == ";") cont(forspec3); 332 | else if (value == "in") cont(expression); 333 | else cont(expression, expect(";"), forspec3); 334 | } 335 | function forspec3(type) { 336 | if (type == ")") pass(); 337 | else cont(expression); 338 | } 339 | // A function definition creates a new context, and the variables 340 | // in its argument list have to be added to this context. 341 | function functiondef(type, value){ 342 | if (type == "variable"){register(value); cont(functiondef);} 343 | else if (type == "(") cont(pushcontext, commasep(funarg, ")"), statement, popcontext); 344 | } 345 | function funarg(type, value){ 346 | if (type == "variable"){register(value); cont();} 347 | } 348 | 349 | return parser; 350 | } 351 | 352 | return { 353 | make: parseJS, 354 | electricChars: "{}:", 355 | configure: function(obj) { 356 | if (obj.json != null) json = obj.json; 357 | } 358 | }; 359 | })(); 360 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/js/stringstream.js: -------------------------------------------------------------------------------- 1 | /* String streams are the things fed to parsers (which can feed them 2 | * to a tokenizer if they want). They provide peek and next methods 3 | * for looking at the current character (next 'consumes' this 4 | * character, peek does not), and a get method for retrieving all the 5 | * text that was consumed since the last time get was called. 6 | * 7 | * An easy mistake to make is to let a StopIteration exception finish 8 | * the token stream while there are still characters pending in the 9 | * string stream (hitting the end of the buffer while parsing a 10 | * token). To make it easier to detect such errors, the stringstreams 11 | * throw an exception when this happens. 12 | */ 13 | 14 | // Make a stringstream stream out of an iterator that returns strings. 15 | // This is applied to the result of traverseDOM (see codemirror.js), 16 | // and the resulting stream is fed to the parser. 17 | var stringStream = function(source){ 18 | // String that's currently being iterated over. 19 | var current = ""; 20 | // Position in that string. 21 | var pos = 0; 22 | // Accumulator for strings that have been iterated over but not 23 | // get()-ed yet. 24 | var accum = ""; 25 | // Make sure there are more characters ready, or throw 26 | // StopIteration. 27 | function ensureChars() { 28 | while (pos == current.length) { 29 | accum += current; 30 | current = ""; // In case source.next() throws 31 | pos = 0; 32 | try {current = source.next();} 33 | catch (e) { 34 | if (e != StopIteration) throw e; 35 | else return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | return { 42 | // peek: -> character 43 | // Return the next character in the stream. 44 | peek: function() { 45 | if (!ensureChars()) return null; 46 | return current.charAt(pos); 47 | }, 48 | // next: -> character 49 | // Get the next character, throw StopIteration if at end, check 50 | // for unused content. 51 | next: function() { 52 | if (!ensureChars()) { 53 | if (accum.length > 0) 54 | throw "End of stringstream reached without emptying buffer ('" + accum + "')."; 55 | else 56 | throw StopIteration; 57 | } 58 | return current.charAt(pos++); 59 | }, 60 | // get(): -> string 61 | // Return the characters iterated over since the last call to 62 | // .get(). 63 | get: function() { 64 | var temp = accum; 65 | accum = ""; 66 | if (pos > 0){ 67 | temp += current.slice(0, pos); 68 | current = current.slice(pos); 69 | pos = 0; 70 | } 71 | return temp; 72 | }, 73 | // Push a string back into the stream. 74 | push: function(str) { 75 | current = current.slice(0, pos) + str + current.slice(pos); 76 | }, 77 | lookAhead: function(str, consume, skipSpaces, caseInsensitive) { 78 | function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} 79 | str = cased(str); 80 | var found = false; 81 | 82 | var _accum = accum, _pos = pos; 83 | if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/); 84 | 85 | while (true) { 86 | var end = pos + str.length, left = current.length - pos; 87 | if (end <= current.length) { 88 | found = str == cased(current.slice(pos, end)); 89 | pos = end; 90 | break; 91 | } 92 | else if (str.slice(0, left) == cased(current.slice(pos))) { 93 | accum += current; current = ""; 94 | try {current = source.next();} 95 | catch (e) {break;} 96 | pos = 0; 97 | str = str.slice(left); 98 | } 99 | else { 100 | break; 101 | } 102 | } 103 | 104 | if (!(found && consume)) { 105 | current = accum.slice(_accum.length) + current; 106 | pos = _pos; 107 | accum = _accum; 108 | } 109 | 110 | return found; 111 | }, 112 | 113 | // Utils built on top of the above 114 | // more: -> boolean 115 | // Produce true if the stream isn't empty. 116 | more: function() { 117 | return this.peek() !== null; 118 | }, 119 | applies: function(test) { 120 | var next = this.peek(); 121 | return (next !== null && test(next)); 122 | }, 123 | nextWhile: function(test) { 124 | var next; 125 | while ((next = this.peek()) !== null && test(next)) 126 | this.next(); 127 | }, 128 | matches: function(re) { 129 | var next = this.peek(); 130 | return (next !== null && re.test(next)); 131 | }, 132 | nextWhileMatches: function(re) { 133 | var next; 134 | while ((next = this.peek()) !== null && re.test(next)) 135 | this.next(); 136 | }, 137 | equals: function(ch) { 138 | return ch === this.peek(); 139 | }, 140 | endOfLine: function() { 141 | var next = this.peek(); 142 | return next == null || next == "\n"; 143 | } 144 | }; 145 | }; 146 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/js/tokenize.js: -------------------------------------------------------------------------------- 1 | // A framework for simple tokenizers. Takes care of newlines and 2 | // white-space, and of getting the text from the source stream into 3 | // the token object. A state is a function of two arguments -- a 4 | // string stream and a setState function. The second can be used to 5 | // change the tokenizer's state, and can be ignored for stateless 6 | // tokenizers. This function should advance the stream over a token 7 | // and return a string or object containing information about the next 8 | // token, or null to pass and have the (new) state be called to finish 9 | // the token. When a string is given, it is wrapped in a {style, type} 10 | // object. In the resulting object, the characters consumed are stored 11 | // under the content property. Any whitespace following them is also 12 | // automatically consumed, and added to the value property. (Thus, 13 | // content is the actual meaningful part of the token, while value 14 | // contains all the text it spans.) 15 | 16 | function tokenizer(source, state) { 17 | // Newlines are always a separate token. 18 | function isWhiteSpace(ch) { 19 | // The messy regexp is because IE's regexp matcher is of the 20 | // opinion that non-breaking spaces are no whitespace. 21 | return ch != "\n" && /^[\s\u00a0]*$/.test(ch); 22 | } 23 | 24 | var tokenizer = { 25 | state: state, 26 | 27 | take: function(type) { 28 | if (typeof(type) == "string") 29 | type = {style: type, type: type}; 30 | 31 | type.content = (type.content || "") + source.get(); 32 | if (!/\n$/.test(type.content)) 33 | source.nextWhile(isWhiteSpace); 34 | type.value = type.content + source.get(); 35 | return type; 36 | }, 37 | 38 | next: function () { 39 | if (!source.more()) throw StopIteration; 40 | 41 | var type; 42 | if (source.equals("\n")) { 43 | source.next(); 44 | return this.take("whitespace"); 45 | } 46 | 47 | if (source.applies(isWhiteSpace)) 48 | type = "whitespace"; 49 | else 50 | while (!type) 51 | type = this.state(source, function(s) {tokenizer.state = s;}); 52 | 53 | return this.take(type); 54 | } 55 | }; 56 | return tokenizer; 57 | } 58 | -------------------------------------------------------------------------------- /remotejs/src/desktop/codemirror/js/tokenizejavascript.js: -------------------------------------------------------------------------------- 1 | /* Tokenizer for JavaScript code */ 2 | 3 | var tokenizeJavaScript = (function() { 4 | // Advance the stream until the given character (not preceded by a 5 | // backslash) is encountered, or the end of the line is reached. 6 | function nextUntilUnescaped(source, end) { 7 | var escaped = false; 8 | while (!source.endOfLine()) { 9 | var next = source.next(); 10 | if (next == end && !escaped) 11 | return false; 12 | escaped = !escaped && next == "\\"; 13 | } 14 | return escaped; 15 | } 16 | 17 | // A map of JavaScript's keywords. The a/b/c keyword distinction is 18 | // very rough, but it gives the parser enough information to parse 19 | // correct code correctly (we don't care that much how we parse 20 | // incorrect code). The style information included in these objects 21 | // is used by the highlighter to pick the correct CSS style for a 22 | // token. 23 | var keywords = function(){ 24 | function result(type, style){ 25 | return {type: type, style: "js-" + style}; 26 | } 27 | // keywords that take a parenthised expression, and then a 28 | // statement (if) 29 | var keywordA = result("keyword a", "keyword"); 30 | // keywords that take just a statement (else) 31 | var keywordB = result("keyword b", "keyword"); 32 | // keywords that optionally take an expression, and form a 33 | // statement (return) 34 | var keywordC = result("keyword c", "keyword"); 35 | var operator = result("operator", "keyword"); 36 | var atom = result("atom", "atom"); 37 | return { 38 | "if": keywordA, "while": keywordA, "with": keywordA, 39 | "else": keywordB, "do": keywordB, "try": keywordB, "finally": keywordB, 40 | "return": keywordC, "break": keywordC, "continue": keywordC, "new": keywordC, "delete": keywordC, "throw": keywordC, 41 | "in": operator, "typeof": operator, "instanceof": operator, 42 | "var": result("var", "keyword"), "function": result("function", "keyword"), "catch": result("catch", "keyword"), 43 | "for": result("for", "keyword"), "switch": result("switch", "keyword"), 44 | "case": result("case", "keyword"), "default": result("default", "keyword"), 45 | "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom 46 | }; 47 | }(); 48 | 49 | // Some helper regexps 50 | var isOperatorChar = /[+\-*&%=<>!?|]/; 51 | var isHexDigit = /[0-9A-Fa-f]/; 52 | var isWordChar = /[\w\$_]/; 53 | 54 | // Wrapper around jsToken that helps maintain parser state (whether 55 | // we are inside of a multi-line comment and whether the next token 56 | // could be a regular expression). 57 | function jsTokenState(inside, regexp) { 58 | return function(source, setState) { 59 | var newInside = inside; 60 | var type = jsToken(inside, regexp, source, function(c) {newInside = c;}); 61 | var newRegexp = type.type == "operator" || type.type == "keyword c" || type.type.match(/^[\[{}\(,;:]$/); 62 | if (newRegexp != regexp || newInside != inside) 63 | setState(jsTokenState(newInside, newRegexp)); 64 | return type; 65 | }; 66 | } 67 | 68 | // The token reader, intended to be used by the tokenizer from 69 | // tokenize.js (through jsTokenState). Advances the source stream 70 | // over a token, and returns an object containing the type and style 71 | // of that token. 72 | function jsToken(inside, regexp, source, setInside) { 73 | function readHexNumber(){ 74 | source.next(); // skip the 'x' 75 | source.nextWhileMatches(isHexDigit); 76 | return {type: "number", style: "js-atom"}; 77 | } 78 | 79 | function readNumber() { 80 | source.nextWhileMatches(/[0-9]/); 81 | if (source.equals(".")){ 82 | source.next(); 83 | source.nextWhileMatches(/[0-9]/); 84 | } 85 | if (source.equals("e") || source.equals("E")){ 86 | source.next(); 87 | if (source.equals("-")) 88 | source.next(); 89 | source.nextWhileMatches(/[0-9]/); 90 | } 91 | return {type: "number", style: "js-atom"}; 92 | } 93 | // Read a word, look it up in keywords. If not found, it is a 94 | // variable, otherwise it is a keyword of the type found. 95 | function readWord() { 96 | source.nextWhileMatches(isWordChar); 97 | var word = source.get(); 98 | var known = keywords.hasOwnProperty(word) && keywords.propertyIsEnumerable(word) && keywords[word]; 99 | return known ? {type: known.type, style: known.style, content: word} : 100 | {type: "variable", style: "js-variable", content: word}; 101 | } 102 | function readRegexp() { 103 | nextUntilUnescaped(source, "/"); 104 | source.nextWhileMatches(/[gi]/); 105 | return {type: "regexp", style: "js-string"}; 106 | } 107 | // Mutli-line comments are tricky. We want to return the newlines 108 | // embedded in them as regular newline tokens, and then continue 109 | // returning a comment token for every line of the comment. So 110 | // some state has to be saved (inside) to indicate whether we are 111 | // inside a /* */ sequence. 112 | function readMultilineComment(start){ 113 | var newInside = "/*"; 114 | var maybeEnd = (start == "*"); 115 | while (true) { 116 | if (source.endOfLine()) 117 | break; 118 | var next = source.next(); 119 | if (next == "/" && maybeEnd){ 120 | newInside = null; 121 | break; 122 | } 123 | maybeEnd = (next == "*"); 124 | } 125 | setInside(newInside); 126 | return {type: "comment", style: "js-comment"}; 127 | } 128 | function readOperator() { 129 | source.nextWhileMatches(isOperatorChar); 130 | return {type: "operator", style: "js-operator"}; 131 | } 132 | function readString(quote) { 133 | var endBackSlash = nextUntilUnescaped(source, quote); 134 | setInside(endBackSlash ? quote : null); 135 | return {type: "string", style: "js-string"}; 136 | } 137 | 138 | // Fetch the next token. Dispatches on first character in the 139 | // stream, or first two characters when the first is a slash. 140 | if (inside == "\"" || inside == "'") 141 | return readString(inside); 142 | var ch = source.next(); 143 | if (inside == "/*") 144 | return readMultilineComment(ch); 145 | else if (ch == "\"" || ch == "'") 146 | return readString(ch); 147 | // with punctuation, the type of the token is the symbol itself 148 | else if (/[\[\]{}\(\),;\:\.]/.test(ch)) 149 | return {type: ch, style: "js-punctuation"}; 150 | else if (ch == "0" && (source.equals("x") || source.equals("X"))) 151 | return readHexNumber(); 152 | else if (/[0-9]/.test(ch)) 153 | return readNumber(); 154 | else if (ch == "/"){ 155 | if (source.equals("*")) 156 | { source.next(); return readMultilineComment(ch); } 157 | else if (source.equals("/")) 158 | { nextUntilUnescaped(source, null); return {type: "comment", style: "js-comment"};} 159 | else if (regexp) 160 | return readRegexp(); 161 | else 162 | return readOperator(); 163 | } 164 | else if (isOperatorChar.test(ch)) 165 | return readOperator(); 166 | else 167 | return readWord(); 168 | } 169 | 170 | // The external interface to the tokenizer. 171 | return function(source, startState) { 172 | return tokenizer(source, startState || jsTokenState(false, true)); 173 | }; 174 | })(); 175 | -------------------------------------------------------------------------------- /remotejs/src/desktop/generate.dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | make clean 3 | rm -rf RemoteJS.app 4 | qmake 5 | make 6 | macdeployqt RemoteJS.app -dmg 7 | -------------------------------------------------------------------------------- /remotejs/src/desktop/interface.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 70 |
71 |
72 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /remotejs/src/desktop/interface.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | var MAX_HISTORY_SIZE = 100; 24 | var DEVICE_RESCAN_TIMEOUT = 2000; 25 | 26 | var expressionHistory = []; 27 | var currentExpressionIndex = -1; 28 | var isJustLaunched = true; 29 | var liveExpression = ""; 30 | var rescanTimeoutId = null; 31 | 32 | window.onresize = function() { 33 | var header = document.getElementById('header'); 34 | document.body.style.paddingTop = header.offsetHeight; 35 | } 36 | 37 | function loadHistory() { 38 | var i; 39 | var length = localStorage["HistoryLength"]; 40 | for (i = 0; i < length; ++i) 41 | expressionHistory.push(localStorage["HistoryExpression" + i]); 42 | } 43 | 44 | function addToHistory(s) { 45 | if (s == expressionHistory[expressionHistory.length - 1]) 46 | return; 47 | expressionHistory.push(s); 48 | currentExpressionIndex = -1; 49 | if (expressionHistory.length > MAX_HISTORY_SIZE) 50 | expressionHistory.shift(); 51 | } 52 | 53 | function getPreviousExpression() { 54 | if (currentExpressionIndex == 0 || expressionHistory.length < 2) 55 | return expressionHistory[currentExpressionIndex]; 56 | else if (currentExpressionIndex == -1) 57 | currentExpressionIndex = expressionHistory.length - 1; 58 | else 59 | --currentExpressionIndex; 60 | return expressionHistory[currentExpressionIndex]; 61 | } 62 | 63 | function getNextExpression() { 64 | if (currentExpressionIndex == -1) 65 | return liveExpression; 66 | else if (currentExpressionIndex == (expressionHistory.length - 1)) { 67 | currentExpressionIndex = -1; 68 | return liveExpression; 69 | } else { 70 | ++currentExpressionIndex; 71 | return expressionHistory[currentExpressionIndex]; 72 | } 73 | } 74 | 75 | function saveHistory() { 76 | var i; 77 | var length = expressionHistory.length; 78 | for (i = 0; i < length; ++i) 79 | localStorage["HistoryExpression" + i] = expressionHistory[i]; 80 | localStorage["HistoryLength"] = length; 81 | } 82 | 83 | function aboutToQuit() { 84 | saveHistory(); 85 | remoteConsole.disconnected.disconnect(gotDisconnected); 86 | } 87 | 88 | function addHorizontalRule() { 89 | if (isJustLaunched) 90 | return; 91 | var hr = document.createElement('hr'); 92 | hr.setAttribute('size', '1'); 93 | hr.setAttribute('color', '#F0F0F0'); 94 | document.getElementById('content').appendChild(hr); 95 | } 96 | 97 | function addJSCommand(s) { 98 | addHorizontalRule(); 99 | var content = document.getElementById('content'); 100 | var e = document.createElement('code'); 101 | highlightText('> ' + s, e); 102 | content.appendChild(e); 103 | window.scrollTo(0, 1e9); 104 | } 105 | 106 | function addJSResult(s) { 107 | var content = document.getElementById('content'); 108 | var e = document.createElement('code'); 109 | if (s.match("^[a-z A-Z]*Error:")) { 110 | e.setAttribute('style', 'color: red'); 111 | e.innerText = s; 112 | } else 113 | highlightText(s, e); 114 | content.appendChild(e); 115 | window.scrollTo(0, 1e9); 116 | } 117 | 118 | function addLaunchURL(s) { 119 | addHorizontalRule(); 120 | var e = document.createElement('p'); 121 | e.setAttribute('style', 'color: deepskyblue'); 122 | e.appendChild(document.createTextNode('Opening ' + s)); 123 | document.getElementById('content').appendChild(e); 124 | window.scrollTo(0, 1e9); 125 | } 126 | 127 | function execute() { 128 | var cmd = document.getElementById('userinput').value.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // trim 129 | if (cmd == "") 130 | return false; 131 | var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ 132 | if (cmd.match("^www.") || regexp.test(cmd)) { 133 | addLaunchURL(cmd); 134 | remoteConsole.openUrl(cmd); 135 | } else { 136 | remoteConsole.evaluateJavaScript(cmd); 137 | addJSCommand(cmd); 138 | } 139 | addToHistory(cmd); 140 | document.getElementById('userinput').value = ""; 141 | if (isJustLaunched) 142 | isJustLaunched = !isJustLaunched; 143 | return false; 144 | } 145 | 146 | function handleKey(e) { 147 | var evt = e || window.event; 148 | 149 | // Hack to work around Shift-9 (opening paranthesis) reporting keycode 40 150 | if (evt.shiftKey) 151 | return; 152 | 153 | switch (evt.keyCode) { 154 | case 33: // page up 155 | case 38: // arrow up 156 | var s = getPreviousExpression(); 157 | document.getElementById('userinput').value = s; 158 | break; 159 | case 34: // page down 160 | case 40: // arrow down 161 | var s = getNextExpression(); 162 | document.getElementById('userinput').value = s; 163 | break; 164 | case 37: // arrow left 165 | case 39: // arrow right 166 | break; 167 | default: 168 | var element = document.getElementById('userinput'); 169 | var value = element.value; 170 | var caret = element.selectionStart; 171 | var char = String.fromCharCode(evt.charCode); 172 | liveExpression = value.substring(0, caret) + char + value.substring(caret); 173 | currentExpressionIndex = -1; 174 | } 175 | window.scrollTo(0, 1e9); 176 | } 177 | 178 | function showError(msg) { 179 | var e = document.getElementById('error'); 180 | if (!e) { 181 | e = document.createElement('p'); 182 | e.setAttribute('style', 'color: orangered'); 183 | e.setAttribute('id', 'error'); 184 | e.appendChild(document.createTextNode(msg)); 185 | document.getElementById('header').appendChild(e); 186 | } else { 187 | e.firstChild.nodeValue = msg; 188 | e.style.display = 'block'; 189 | } 190 | window.onresize(); 191 | } 192 | 193 | function hideError() { 194 | var e = document.getElementById('error'); 195 | if (e) 196 | e.style.display = 'none'; 197 | } 198 | 199 | function gotDisconnected() { 200 | remoteConsole.dataAvailable.disconnect(addJSResult); 201 | remoteConsole.aboutToQuit.disconnect(aboutToQuit); 202 | remoteConsole.disconnected.disconnect(gotDisconnected); 203 | remoteConsole.targetDevice = ''; 204 | 205 | showError('adb (Android Debug Bridge) is disconnected!'); 206 | 207 | document.getElementById('entry').style.display = 'none'; 208 | document.getElementById('help').style.display = 'none'; 209 | var dev = document.getElementById('devicename'); 210 | if (dev) 211 | dev.style.display = 'none'; 212 | 213 | showDeviceMenu(); 214 | } 215 | 216 | function selectDevice(device) { 217 | remoteConsole.dataAvailable.connect(addJSResult); 218 | remoteConsole.aboutToQuit.connect(aboutToQuit); 219 | remoteConsole.disconnected.connect(gotDisconnected); 220 | remoteConsole.targetDevice = device; 221 | 222 | var devicename = document.getElementById('devicename'); 223 | if (devicename) { 224 | devicename.style.display = 'block'; 225 | devicename.firstChild.nodeValue = "Target device is " + device + "."; 226 | } else { 227 | devicename = document.createElement('p'); 228 | devicename.setAttribute('style', 'color: lightgreen'); 229 | devicename.setAttribute('id', 'devicename'); 230 | devicename.appendChild(document.createTextNode("Target device is " + device + ".")); 231 | document.getElementById('header').appendChild(devicename); 232 | } 233 | 234 | document.getElementById('devicelist').style.display = 'none'; 235 | document.getElementById('entry').style.display = 'inline'; 236 | document.getElementById('help').style.display = 'inline'; 237 | document.getElementById('userinput').onkeydown = handleKey; 238 | document.getElementById('userinput').onkeypress = handleKey; 239 | 240 | hideError(); 241 | 242 | var msg = remoteConsole.installDeviceTool(); 243 | if (msg && msg != "Success") 244 | showError("Unable to auto-install host tool: " + msg); 245 | } 246 | 247 | function showDeviceMenu() { 248 | document.getElementById('devicelist').style.display = 'inline'; 249 | var menu = document.getElementById('devicemenu'); 250 | var list = remoteConsole.deviceList; 251 | 252 | if (list.length == 0) { 253 | showError('No devices found. Please connect your device.'); 254 | document.getElementById('devices').style.display = 'none'; 255 | 256 | while (menu.hasChildNodes()) 257 | menu.removeChild(menu.firstChild); 258 | 259 | rescanTimeoutId = setTimeout("showDeviceMenu()", DEVICE_RESCAN_TIMEOUT); 260 | } else { 261 | if (rescanTimeoutId != null) { 262 | clearTimeout(rescanTimeoutId); 263 | rescanTimeoutId = null; 264 | } 265 | 266 | if (list.length == 1) { 267 | selectDevice(list[0]); 268 | } else { 269 | // Remove any old children added by a previous call to this function 270 | while (menu.hasChildNodes()) 271 | menu.removeChild(menu.firstChild); 272 | 273 | for (i = 0; i < list.length; ++i) { 274 | var b = document.createElement('button'); 275 | b.setAttribute('onClick', 'selectDevice("' + list[i] + '")'); 276 | b.className = "device"; 277 | var t = document.createTextNode(list[i]); 278 | b.appendChild(t); 279 | menu.appendChild(b); 280 | } 281 | } 282 | loadHistory(); 283 | } 284 | window.onresize(); 285 | } 286 | 287 | var adbPath = localStorage.adbPath; 288 | if (adbPath != undefined) { 289 | remoteConsole.adbPath = adbPath; 290 | } 291 | 292 | if (!remoteConsole.isAdbAvailable) { 293 | adbPath = remoteConsole.chooseAdbPath(); 294 | remoteConsole.adbPath = adbPath; 295 | if (!remoteConsole.isAdbAvailable) { 296 | showError('adb (Android Debug Bridge) is not available. Please set PATH to the Android SDK installation!'); 297 | } else { 298 | showDeviceMenu(); 299 | localStorage.adbPath = adbPath; 300 | } 301 | } else { 302 | showDeviceMenu(); 303 | } 304 | -------------------------------------------------------------------------------- /remotejs/src/desktop/remotejs.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Sencha Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | #include 24 | #include 25 | 26 | #define LOG_FILTER "RemoteJS" 27 | #define TARGET_ACTIVITY "com.sencha.remotejs/.RemoteJS" 28 | 29 | class RemoteConsole: public QObject 30 | { 31 | Q_OBJECT 32 | Q_PROPERTY(bool isAdbAvailable READ isAdbAvailable) 33 | Q_PROPERTY(QStringList deviceList READ getDeviceList) 34 | Q_PROPERTY(QString targetDevice READ targetDevice WRITE setTargetDevice) 35 | Q_PROPERTY(QString adbPath READ adbPath WRITE setAdbPath) 36 | 37 | public: 38 | explicit RemoteConsole(QObject *parent = 0); 39 | ~RemoteConsole(); 40 | 41 | bool isAdbAvailable() const; 42 | QString targetDevice() const; 43 | 44 | signals: 45 | void aboutToQuit(); 46 | void dataAvailable(const QString &data); 47 | void disconnected(); 48 | 49 | public slots: 50 | void evaluateJavaScript(const QString &script); 51 | QStringList getDeviceList(); 52 | QString installDeviceTool(); 53 | void openUrl(const QString &url); 54 | void readAdbData(); 55 | void setAdbPath(const QString &adbPath); 56 | QString adbPath() const; 57 | void setTargetDevice(const QString &device); 58 | QString chooseAdbPath(); 59 | void captureWindow(); 60 | void saveCapture(); 61 | 62 | private: 63 | QProcess m_adbConnection; 64 | QString m_adbData; 65 | QString m_adbPath; 66 | QString m_targetDevice; 67 | 68 | void adbExecute(const QString &command) const; 69 | QString adbQuery(const QString &command) const; 70 | QString readProcess(const QString &command, int timeout = 30000) const; 71 | QString readProcess(QProcess &process, const QString &command, int timeout = 30000) const; 72 | }; 73 | 74 | RemoteConsole::RemoteConsole(QObject *parent) 75 | : QObject(parent) 76 | { 77 | connect(&m_adbConnection, SIGNAL(readyRead()), this, SLOT(readAdbData())); 78 | connect(&m_adbConnection, SIGNAL(finished(int, QProcess::ExitStatus)), SIGNAL(disconnected())); 79 | connect(qApp, SIGNAL(aboutToQuit()), SIGNAL(aboutToQuit())); 80 | } 81 | 82 | RemoteConsole::~RemoteConsole() 83 | { 84 | m_adbConnection.close(); 85 | } 86 | 87 | QString RemoteConsole::installDeviceTool() 88 | { 89 | QFile temp(QDir::temp().absoluteFilePath("RemoteJS.apk")); 90 | if (!temp.open(QFile::WriteOnly)) 91 | return QLatin1String("Unable to open temp file: ") + temp.fileName(); 92 | QFile tool(":/RemoteJS.apk"); 93 | if (!tool.open(QFile::ReadOnly)) 94 | return QLatin1String("Unable to open tool from the resource subsystem"); 95 | QByteArray rd; 96 | while (!tool.atEnd()) { 97 | rd = tool.read(8192); 98 | temp.write(rd); 99 | } 100 | tool.close(); 101 | temp.close(); 102 | 103 | adbQuery("uninstall com.sencha.remotejs"); 104 | QString ret = adbQuery("install " + temp.fileName()); 105 | 106 | temp.remove(); 107 | 108 | return ret.trimmed().split(QLatin1Char('\n')).last().remove(QLatin1Char('\r')); 109 | } 110 | 111 | void RemoteConsole::readAdbData() 112 | { 113 | m_adbData += m_adbConnection.readAll(); 114 | int pos = m_adbData.indexOf(QLatin1Char('\n')); 115 | while (pos >= 0) { 116 | QString output = m_adbData.left(pos); 117 | 118 | m_adbData.remove(0, pos + 1); 119 | pos = m_adbData.indexOf(QLatin1Char('\n')); 120 | 121 | if (!output.contains(QRegExp(QLatin1String("[A-Z]/") + LOG_FILTER))) 122 | continue; 123 | 124 | bool info = output[0] == 'I'; 125 | 126 | output.remove(QRegExp(QLatin1String("[A-Z]/") + LOG_FILTER 127 | + QLatin1String("(\\b)*\\((\\s)*(\\d)+\\): "))); 128 | output.remove(QRegExp(QLatin1String("Console: "))); 129 | output.remove(QRegExp(QLatin1String(":(\\d)+(\\b)*"))); 130 | output.remove('\r'); 131 | 132 | if (info) { 133 | if (output.startsWith("Capture start")) 134 | qApp->setOverrideCursor(Qt::WaitCursor); 135 | if (output.startsWith("Capture saved")) { 136 | qApp->restoreOverrideCursor(); 137 | QTimer::singleShot(0, this, SLOT(saveCapture())); 138 | } 139 | if (output.startsWith("Capture error")) { 140 | qApp->restoreOverrideCursor(); 141 | QMessageBox::warning(0, "Capture fails", output); 142 | } 143 | } else { 144 | emit dataAvailable(output); 145 | } 146 | } 147 | } 148 | 149 | void RemoteConsole::adbExecute(const QString &command) const 150 | { 151 | qApp->setOverrideCursor(Qt::WaitCursor); 152 | QString device; 153 | if (!m_targetDevice.isEmpty()) 154 | device = QLatin1String(" -s ") + m_targetDevice; 155 | QProcess::execute(m_adbPath + device + ' ' + command); 156 | qApp->restoreOverrideCursor(); 157 | } 158 | 159 | QString RemoteConsole::adbQuery(const QString &command) const 160 | { 161 | qApp->setOverrideCursor(Qt::WaitCursor); 162 | QString device; 163 | if (!m_targetDevice.isEmpty()) 164 | device = QLatin1String(" -s ") + m_targetDevice; 165 | QString result = readProcess(m_adbPath + device + ' ' + command); 166 | qApp->restoreOverrideCursor(); 167 | return result; 168 | } 169 | 170 | QString RemoteConsole::readProcess(const QString &command, int timeout) const 171 | { 172 | QProcess process; 173 | return readProcess(process, command, timeout); 174 | } 175 | 176 | QString RemoteConsole::readProcess(QProcess &process, const QString &command, 177 | int timeout) const 178 | { 179 | qApp->setOverrideCursor(Qt::WaitCursor); 180 | process.start(command); 181 | process.waitForFinished(timeout); 182 | QString output = process.readAll(); 183 | process.close(); 184 | qApp->restoreOverrideCursor(); 185 | return output; 186 | } 187 | 188 | bool RemoteConsole::isAdbAvailable() const 189 | { 190 | QString version = adbQuery(QLatin1String("version")); 191 | return version.startsWith(QLatin1String("Android Debug Bridge")); 192 | } 193 | 194 | QStringList RemoteConsole::getDeviceList() 195 | { 196 | QString devices = adbQuery(QLatin1String("devices")); 197 | devices.remove(QRegExp(QLatin1String("List of devices attached\\s+"))); 198 | 199 | QStringList temp = devices.split('\n', QString::SkipEmptyParts); 200 | QStringList list; 201 | QRegExp re(QLatin1String("\\tdevice")); 202 | foreach (QString str, temp) { 203 | if (str.contains(re)) 204 | list << str.remove(re); 205 | } 206 | 207 | return list; 208 | } 209 | 210 | void RemoteConsole::setAdbPath(const QString &adbPath) 211 | { 212 | m_adbPath = adbPath; 213 | } 214 | 215 | QString RemoteConsole::adbPath() const 216 | { 217 | return m_adbPath; 218 | } 219 | 220 | QString RemoteConsole::targetDevice() const 221 | { 222 | return m_targetDevice; 223 | } 224 | 225 | void RemoteConsole::setTargetDevice(const QString &device) 226 | { 227 | qApp->setOverrideCursor(Qt::WaitCursor); 228 | m_targetDevice = device; 229 | bool enabled = !m_targetDevice.isEmpty(); 230 | if (enabled) { 231 | QString adbDevice = m_adbPath + QLatin1String(" -s ") + m_targetDevice; 232 | QProcess::execute(adbDevice + QLatin1String(" logcat -c")); // flush log 233 | m_adbConnection.start(adbDevice + QLatin1String(" logcat ") + LOG_FILTER 234 | + QLatin1String(":V *:S")); 235 | } 236 | qApp->restoreOverrideCursor(); 237 | } 238 | 239 | QString RemoteConsole::chooseAdbPath() 240 | { 241 | return QFileDialog::getOpenFileName(0, QLatin1String("Select the adb executable"), m_adbPath); 242 | } 243 | 244 | void RemoteConsole::openUrl(const QString &address) 245 | { 246 | QString location = QUrl::fromUserInput(address).toEncoded().toBase64(); 247 | QString command = QLatin1String(" shell am start -a android.intent.action.VIEW -n ") 248 | + TARGET_ACTIVITY + QLatin1String(" -d '") + location + QLatin1String("'"); 249 | adbExecute(command); 250 | } 251 | 252 | void RemoteConsole::evaluateJavaScript(const QString &script) 253 | { 254 | QString expression = QByteArray("javascript:(function() { " + script.toLatin1() 255 | + "; })()").toBase64(); 256 | QString command = QLatin1String(" shell am start -a android.intent.action.VIEW -n ") 257 | + TARGET_ACTIVITY + QLatin1String(" -d '") + expression + QLatin1String("'"); 258 | adbExecute(command); 259 | } 260 | 261 | void RemoteConsole::captureWindow() 262 | { 263 | QString command = QLatin1String(" shell am start -a com.sencha.remotejs.ACTION_CAPTURE -n ") 264 | + TARGET_ACTIVITY; 265 | adbExecute(command); 266 | } 267 | 268 | void RemoteConsole::saveCapture() 269 | { 270 | QString name = QFileDialog::getSaveFileName(qobject_cast(parent()), 271 | QLatin1String("Save Capture (PNG)"), 272 | QString(), "Images (*.png)"); 273 | if (name.isEmpty()) 274 | return; 275 | QString command = QLatin1String(" pull /data/data/com.sencha.remotejs/cache/remotejs-capture.png ") 276 | + name; 277 | adbExecute(command); 278 | } 279 | 280 | static QString readFileContents(const QString &fileName) 281 | { 282 | QFile file(fileName); 283 | file.open(QFile::ReadOnly); 284 | QString contents = file.readAll(); 285 | file.close(); 286 | return contents; 287 | } 288 | 289 | int main(int argc, char *argv[]) 290 | { 291 | QApplication::setGraphicsSystem("raster"); 292 | QApplication application(argc, argv); 293 | 294 | #ifdef Q_OS_MAC 295 | QWebSettings::globalSettings()->setFontFamily(QWebSettings::FixedFont, "Monaco"); 296 | #endif 297 | 298 | QWebView webView; 299 | RemoteConsole remoteConsole(&webView); 300 | webView.settings()->setAttribute(QWebSettings::LocalStorageEnabled, true); 301 | webView.settings()->setLocalStoragePath(QDesktopServices::storageLocation(QDesktopServices::DataLocation)); 302 | webView.page()->mainFrame()->setHtml(readFileContents(":/interface.html")); 303 | webView.page()->mainFrame()->addToJavaScriptWindowObject("remoteConsole", &remoteConsole); 304 | webView.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); 305 | webView.setWindowTitle("RemoteJS - Remote JavaScript Console for Android"); 306 | webView.show(); 307 | 308 | return application.exec(); 309 | } 310 | 311 | #include "remotejs.moc" 312 | -------------------------------------------------------------------------------- /remotejs/src/desktop/remotejs.pro: -------------------------------------------------------------------------------- 1 | QT += core gui network webkit 2 | TARGET = remotejs 3 | TEMPLATE = app 4 | SOURCES += remotejs.cpp 5 | RESOURCES += remotejs.qrc 6 | -------------------------------------------------------------------------------- /remotejs/src/desktop/remotejs.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | interface.html 4 | interface.js 5 | 6 | codemirror/css/jscolors.css 7 | codemirror/js/highlight.js 8 | codemirror/js/parsejavascript.js 9 | codemirror/js/stringstream.js 10 | codemirror/js/tokenize.js 11 | codemirror/js/tokenizejavascript.js 12 | 13 | sencha_logo.png 14 | 15 | ../android/bin/RemoteJS.apk 16 | 17 | 18 | -------------------------------------------------------------------------------- /remotejs/src/desktop/sencha_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senchalabs/android-tools/895c92952855c3df8ccdd689ac4e788afc9a46cf/remotejs/src/desktop/sencha_logo.png -------------------------------------------------------------------------------- /remotejs/src/shell/README.md: -------------------------------------------------------------------------------- 1 | RemoteJS shell application 2 | === 3 | 4 | What it is 5 | --- 6 | 7 | It's the non-GUI version of RemoteJS. 8 | 9 | How to build 10 | --- 11 | 12 | Since it's written in the Python programming language, it doesn't require any build. 13 | Python version 2.6 or above is required to run it though. 14 | To run, type `python remotejs.py`. For help on usage, add the `-h` or `--help` option. 15 | -------------------------------------------------------------------------------- /remotejs/src/shell/remotejs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2010 Sencha Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import adb 26 | import os 27 | import re 28 | import sys 29 | import thread 30 | 31 | OPTION_DEVICE = "-s" 32 | OPTION_HELP = "-h" 33 | OPTION_HELP_LONG = "--help" 34 | OPTION_NOHOSTUPDATE = "-n" 35 | 36 | class ExitCode: 37 | Undefined = -100 38 | Help = -10 39 | Normal = 0 40 | AdbNotFound = 5 41 | NoDevices = 15 42 | DeviceDisconnected = 25 43 | MultipleDevices = 35 44 | Aborted = 45 45 | DeviceToolFailed = 55 46 | WrongUsage = 65 47 | UnknownDevice = 75 48 | BadSleepValue = 85 49 | 50 | _g_exitCode = ExitCode.Undefined 51 | 52 | def exitCode(): 53 | return _g_exitCode 54 | 55 | def setExitCode(err): 56 | global _g_exitCode 57 | _g_exitCode = err 58 | 59 | def printError(msg): 60 | print >> sys.stderr, "#", msg 61 | 62 | def printInfo(msg): 63 | print "#", msg 64 | 65 | def printUsage(): 66 | app = os.path.basename(sys.argv[0]) 67 | print "Usage: ", app, "\t\t- assume one attached device only" 68 | print " ", app, OPTION_DEVICE, "\t\t- connect to device with serial number " 69 | print " ", app, OPTION_NOHOSTUPDATE, "\t\t- keep existing host tool (advanced)" 70 | print " ", app, OPTION_HELP, "|", OPTION_HELP_LONG, "\t- print this help" 71 | 72 | def logcatHandler(line): 73 | print adb.filterLogcat(line) 74 | 75 | def logcatThread(): 76 | adb.readLogcat(logcatHandler) 77 | setExitCode(ExitCode.DeviceDisconnected) 78 | thread.interrupt_main() 79 | 80 | def execute(expr): 81 | fullUrlRe = r"^(ftp|http|https)://(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(/|/([\w#!:.?+=&%@!-/]))?" 82 | if expr.startswith("www") and re.match(fullUrlRe, "http://" + expr): 83 | adb.openUrl("http://" + expr) 84 | elif re.match(fullUrlRe, expr): 85 | adb.openUrl(expr) 86 | else: 87 | adb.evaluateJS(expr) 88 | 89 | def inputThread(): 90 | expr = "" 91 | try: 92 | while True: 93 | expr = raw_input().strip() 94 | if expr == "": 95 | continue 96 | else: 97 | execute(expr) 98 | except: 99 | setExitCode(ExitCode.Normal) 100 | thread.interrupt_main() 101 | 102 | def mainThread(): 103 | args = sys.argv[1:] 104 | 105 | if OPTION_HELP in args or OPTION_HELP_LONG in args: 106 | printUsage() 107 | return ExitCode.Help 108 | 109 | if not adb.isAvailable(): 110 | printError("'adb' not found, please add its location to $PATH.") 111 | return ExitCode.AdbNotFound 112 | 113 | adb.startServer() 114 | devices = adb.devices() 115 | 116 | if len(devices) == 0: 117 | printError("No attached devices.") 118 | return ExitCode.NoDevices 119 | 120 | if OPTION_DEVICE in args: 121 | try: 122 | serial = args[args.index(OPTION_DEVICE) + 1] 123 | except IndexError: 124 | printError("Must specify a device serial number.") 125 | return ExitCode.WrongUsage 126 | if serial in devices: 127 | adb.setTargetDevice(serial) 128 | else: 129 | printError("Device " + serial + " not found.") 130 | return ExitCode.UnknownDevice 131 | else: 132 | if len(devices) > 1: 133 | printError("Multiple devices attached, one must be specified.") 134 | return ExitCode.MultipleDevices 135 | 136 | printInfo("RemoteJS - Remote JavaScript Console for Android.") 137 | printInfo("Please wait...") 138 | if not adb.targetDevice(): 139 | adb.setTargetDevice(devices[0]) 140 | 141 | if not OPTION_NOHOSTUPDATE in args: 142 | error = adb.installDeviceTool() 143 | if exitCode() > ExitCode.Normal: 144 | if exitCode() == ExitCode.DeviceToolFailed: 145 | printError("Device tool installation failed - " + error) 146 | else: 147 | printError("Aborted while installing host tool.") 148 | return exitCode() 149 | 150 | printInfo("Target device is " + adb.targetDevice() + ".") 151 | 152 | thread.start_new_thread(logcatThread, ()) 153 | thread.start_new_thread(inputThread, ()) 154 | 155 | try: 156 | while True: 157 | pass 158 | except: 159 | if exitCode() == ExitCode.Undefined or exitCode() == ExitCode.Normal: 160 | printInfo("Exiting...") 161 | elif exitCode() == ExitCode.DeviceDisconnected: 162 | printError("Device disconnected.") 163 | return exitCode() 164 | 165 | if __name__ == "__main__": 166 | sys.exit(mainThread()) 167 | -------------------------------------------------------------------------------- /remotejs/src/shell/test_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from adb import * 4 | e = evaluateJS 5 | 6 | import thread 7 | import time 8 | 9 | expected = ['3', 'Map', '0', '1', '2', '1.0.0'] 10 | obtained = [] 11 | 12 | def myFilter(line): 13 | l = filterLogcat(line) 14 | global obtained 15 | obtained += (l,) 16 | return l 17 | 18 | # if there's more than one attached device, one needs to be specified 19 | #setTargetDevice('...') 20 | 21 | # can be commented if device already has the tool 22 | installDeviceTool() 23 | 24 | thread.start_new_thread(readLogcat, (myFilter,)) 25 | openUrl('http://dev.sencha.com/deploy/touch/examples/map/') 26 | time.sleep(5) 27 | 28 | e('console.log(1+2)') 29 | e('console.log(document.title)') 30 | e('for (var i = 0; i < 3; ++i) console.log(i)') 31 | e('console.log(Ext.version)') 32 | 33 | captureWindow('viewport.png') 34 | 35 | errorCount = 0 36 | for i in range(min(len(obtained), len(expected))): 37 | if expected[i] != obtained[i]: 38 | print 'Expected[' + `i` + ']: ' + expected[i] 39 | print 'Obtained[' + `i` + ']: ' + obtained[i] 40 | errorCount += 1 41 | if errorCount: 42 | print 'Found ' + `errorCount` + ' errors in ' + `len(expected)` + ' tests' 43 | -------------------------------------------------------------------------------- /remotejs/src/shell/update-apk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | 5 | if __name__ == "__main__": 6 | fileName = "../android/bin/RemoteJS.apk" 7 | file = open(fileName, "rb") 8 | fileContent = file.read() 9 | file.close() 10 | apk = base64.b64encode(fileContent) 11 | 12 | fileName = "adb.py" 13 | file = open(fileName, "rb") 14 | fileLines = file.readlines() 15 | file.close() 16 | 17 | file = open(fileName, "wb") 18 | for i in range(len(fileLines)): 19 | if fileLines[i].startswith("_g_base64Apk = "): 20 | fileLines[i] = '_g_base64Apk = b"' + apk + '"\n' 21 | break 22 | file.writelines(fileLines) 23 | file.close() 24 | --------------------------------------------------------------------------------