Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add initial version of EventRecorder.

The suite allows you to record test cases for web applications, meaning that
all user interaction with a web page will be recorded. This includes URL loads,
touch events and hardware keyboard events. You can also evaluate JavaScript and
capture screenshots, and the result of these actions will be saved on the host
computer and can later be compared to a previous test run.

EventRecorder consists of a Python script running on a host that records all
touch and hardware keyboard events, as well as allowing the user to perform a
few actions described below.

In addition, there's a Java application running on your target Android device.
The device application uses an Android WebView component that is able to
record and replay all user interaction. This component is the exact same one
used in the Android browser.

The Python script communicates with this application via messages or file
sending over ADB. If you're familiar with our RemoteJS tool, you'll have a
better understanding on how it works already, since we used the same techniques
for this suite.
  • Loading branch information...
commit 895c92952855c3df8ccdd689ac4e788afc9a46cf 1 parent 058864a
Helder Correia authored
Showing with 4,583 additions and 0 deletions.
  1. +107 −0 eventrecorder/README.md
  2. +19 −0 eventrecorder/src/android/AndroidManifest.xml
  3. BIN  eventrecorder/src/android/bin/EventRecorder.apk
  4. +17 −0 eventrecorder/src/android/build.properties
  5. +67 −0 eventrecorder/src/android/build.xml
  6. +11 −0 eventrecorder/src/android/default.properties
  7. BIN  eventrecorder/src/android/res/drawable-mdpi/icon.png
  8. +386 −0 eventrecorder/src/android/res/raw/template.py
  9. +4 −0 eventrecorder/src/android/res/values/strings.xml
  10. +677 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/App.java
  11. +76 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/Base64.java
  12. +90 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/EventWriter.java
  13. +108 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/FileAccess.java
  14. +46 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/HandlerTimer.java
  15. +47 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/PlaybackFileAccess.java
  16. +55 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/Reply.java
  17. +44 −0 eventrecorder/src/android/src/com/sencha/eventrecorder/Storage.java
  18. BIN  eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen1.png
  19. BIN  eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen2.png
  20. BIN  eventrecorder/src/examples/results/forms/nexusone/froyo/screen1.png
  21. BIN  eventrecorder/src/examples/results/forms/nexusone/gingerbread/screen1.png
  22. BIN  eventrecorder/src/examples/results/icons/evo4g/froyo/screen1.png
  23. BIN  eventrecorder/src/examples/results/icons/evo4g/froyo/screen2.png
  24. BIN  eventrecorder/src/examples/results/overlays/screen1.png
  25. BIN  eventrecorder/src/examples/results/overlays/screen2.png
  26. BIN  eventrecorder/src/examples/results/overlays/screen3.png
  27. BIN  eventrecorder/src/examples/results/overlays/screen4.png
  28. +2 −0  eventrecorder/src/examples/results/picker/nexusone/gingerbread/console.log
  29. BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen1.png
  30. BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen2.png
  31. BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen3.png
  32. BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen1.png
  33. BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen2.png
  34. BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen3.png
  35. BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen4.png
  36. BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen5.png
  37. BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen1.png
  38. BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen2.png
  39. BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen3.png
  40. BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen4.png
  41. BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen5.png
  42. +385 −0 eventrecorder/src/examples/tests/carousel.py
  43. +385 −0 eventrecorder/src/examples/tests/forms.py
  44. +385 −0 eventrecorder/src/examples/tests/icons.py
  45. +385 −0 eventrecorder/src/examples/tests/overlays.py
  46. +385 −0 eventrecorder/src/examples/tests/picker.py
  47. +385 −0 eventrecorder/src/examples/tests/tabs2.py
  48. +80 −0 eventrecorder/src/shell/imagediff.py
  49. +24 −0 eventrecorder/src/shell/inline-apk.py
  50. +413 −0 eventrecorder/src/shell/recorder.py
View
107 eventrecorder/README.md
@@ -0,0 +1,107 @@
+EventRecorder
+===
+
+What it is
+---
+_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.
+
+Currently it is possible to:
+
+* Load URLs
+* Execute JavaScript and log evaluations via _console.log()_
+* Record and play back touch and physical keyboard events
+* Capture the device screen leaving status and title bars out
+* Fill text in form fields via the host machine
+
+Supported platforms
+---
+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/).
+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).
+
+Tested Devices
+---
+Please help us grow this list by reporting your working devices:
+
+* Android emulator
+* Droid Incredible
+* Evo 4G
+* Nexus One
+* Nexus S
+* Galaxy Tab (see also _Known Issues_)
+* Xoom
+
+Requirements
+---
+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_.
+To run the shell recorder, a Python (v2.6+) environment is required.
+
+How to record
+---
+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.
+
+ python recorder.py <test>
+
+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:
+
+* __s__ or __screen__: Capture screen.
+* __t__ or __text__: Input text.
+
+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.
+
+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.
+
+All touch events and hardware keyboard events will also be recorded in this stage.
+
+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.
+
+How to play
+---
+Simply execute the script generated by the recorder:
+
+ python <test.py>
+
+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.
+
+Detecting Visual Regressions
+---
+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.
+
+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.
+
+Detecting Non-Visual Regressions
+---
+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.
+
+How it works
+---
+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.
+
+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.
+
+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.
+
+Known Issues
+---
+* Events recorded through soft keyboard typing won't be reproduced in the playback.
+* 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.
+* 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.
+
+Customizing the Android Tool
+---
+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:
+
+ android update project -p .
+
+Please note that the above step only needs to be executed once per workstation. It will generate a file called _local.properties_.
+
+Next, type:
+
+ ant debug
+
+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).
+
+Finally, switch to the [shell](https://github.com/senchalabs/android-tools/blob/master/eventrecorder/src/shell/) directory and run:
+
+ python inline-apk.py
+
+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.
View
19 eventrecorder/src/android/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.sencha.eventrecorder"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:label="@string/app_name" android:icon="@drawable/icon">
+ <activity android:name=".App"
+ android:label="@string/app_name"
+ android:launchMode="singleInstance">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+ <uses-sdk android:minSdkVersion="7" />
+ <uses-permission android:name="android.permission.INTERNET"></uses-permission>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
+</manifest>
View
BIN  eventrecorder/src/android/bin/EventRecorder.apk
Binary file not shown
View
17 eventrecorder/src/android/build.properties
@@ -0,0 +1,17 @@
+# This file is used to override default values used by the Ant build system.
+#
+# This file must be checked in Version Control Systems, as it is
+# integral to the build system of your project.
+
+# This file is only used by the Ant script.
+
+# You can use this to override default values such as
+# 'source.dir' for the location of your java source folder and
+# 'out.dir' for the location of your output folder.
+
+# You can also use it define how the release builds are signed by declaring
+# the following properties:
+# 'key.store' for the location of your keystore and
+# 'key.alias' for the name of the key to use.
+# The password will be asked during the build when you use the 'release' target.
+
View
67 eventrecorder/src/android/build.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="EventRecorder" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked in in Version
+ Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The build.properties file can be created by you and is never touched
+ by the 'android' tool. This is the place to change some of the default property values
+ used by the Ant rules.
+ Here are some properties you may want to change/update:
+
+ application.package
+ the name of your application package as defined in the manifest. Used by the
+ 'uninstall' rule.
+ source.dir
+ the name of the source directory. Default is 'src'.
+ out.dir
+ the name of the output directory. Default is 'bin'.
+
+ Properties related to the SDK location or the project target should be updated
+ using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your application and
+ should be checked in in Version Control Systems.
+
+ -->
+ <property file="build.properties" />
+
+ <!-- The default.properties file is created and updated by the 'android' tool, as well
+ as ADT.
+ This file is an integral part of the build system for your application and
+ should be checked in in Version Control Systems. -->
+ <property file="default.properties" />
+
+ <!-- Custom Android task to deal with the project target, and import the proper rules.
+ This requires ant 1.6.0 or above. -->
+ <path id="android.antlibs">
+ <pathelement path="${sdk.dir}/tools/lib/anttasks.jar" />
+ <pathelement path="${sdk.dir}/tools/lib/sdklib.jar" />
+ <pathelement path="${sdk.dir}/tools/lib/androidprefs.jar" />
+ <pathelement path="${sdk.dir}/tools/lib/apkbuilder.jar" />
+ <pathelement path="${sdk.dir}/tools/lib/jarutils.jar" />
+ </path>
+
+ <taskdef name="setup"
+ classname="com.android.ant.SetupTask"
+ classpathref="android.antlibs" />
+
+ <!-- Execute the Android Setup task that will setup some properties specific to the target,
+ and import the build rules files.
+
+ The rules file is imported from
+ <SDK>/platforms/<target_platform>/templates/android_rules.xml
+
+ To customize some build steps for your project:
+ - copy the content of the main node <project> from android_rules.xml
+ - paste it in this build.xml below the <setup /> task.
+ - disable the import by changing the setup task below to <setup import="false" />
+
+ This will ensure that the properties are setup correctly but that your customized
+ build steps are used.
+ -->
+ <setup />
+
+</project>
View
11 eventrecorder/src/android/default.properties
@@ -0,0 +1,11 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "build.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-7
View
BIN  eventrecorder/src/android/res/drawable-mdpi/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
386 eventrecorder/src/android/res/raw/template.py
@@ -0,0 +1,386 @@
+#!/usr/bin/env python
+
+_g_isPilAvailable = False
+try:
+ from PIL import Image
+ _g_isPilAvailable = True
+except:
+ pass
+
+from subprocess import Popen, PIPE, STDOUT
+import base64
+import math
+import os
+import re
+import socket
+import struct
+import sys
+import tempfile
+import thread
+import time
+
+_OPTION_DEVICE = "-s"
+_OPTION_HELP = "-h"
+
+_TARGET_PACKAGE = 'com.sencha.eventrecorder'
+_TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App'
+_STANDARD_PACKAGE = 'android.intent.action'
+
+_ADB_PORT = 5037
+_LOG_FILTER = 'EventRecorder'
+
+_INTENT_PLAY = "PLAY"
+_INTENT_PUSH_DONE = "PUSH_DONE"
+_INTENT_SCREEN_DONE = "SCREEN_DONE"
+_INTENT_VIEW = "VIEW"
+
+_REPLY_DONE = 'done'
+_REPLY_READY = 'ready'
+_REPLY_SCREEN = 'screen'
+_REPLY_EVENTS_PATH = 'eventsFilePath'
+
+_CONSOLE_LOG_FILE_NAME = "console.log"
+_SCREEN_CAPTURE_PREFIX = "screen"
+_WINDOW_CAPTURE_PREFIX = "window"
+
+class ExitCode:
+ Help = -10
+ Normal = 0
+ AdbNotFound = 5
+ NoDevices = 15
+ DeviceDisconnected = 25
+ MultipleDevices = 35
+ Aborted = 45
+ WrongUsage = 55
+ UnknownDevice = 65
+
+_g_state = {
+ 'exitCode': ExitCode.Normal,
+ 'error': '',
+ 'screenCaptureCount': 0,
+ 'targetDevice': ''
+}
+
+_g_events = '%EVENTS%'
+
+def exitCode():
+ return _g_state['exitCode']
+
+def setExitCode(err):
+ global _g_state
+ _g_state['exitCode'] = err
+
+def error():
+ return _g_state['error']
+
+def setError(err):
+ global _g_state
+ _g_state['error'] = err
+
+def targetDevice():
+ return _g_state['targetDevice']
+
+def setTargetDevice(id):
+ global _g_state
+ _g_state['targetDevice'] = id
+
+def startConnection(port):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ sock.connect(('127.0.0.1', port))
+ return sock
+ except Exception as e:
+ setError('Unable to connect to port %d: %s' % (port, e))
+
+def clearLogcat():
+ cmd = ' logcat -c '
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT)
+ time.sleep(1)
+ proc.kill()
+
+def framebuffer():
+ def headerMap(ints):
+ if len(ints) == 12:
+ return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3],
+ 'red': {'offset': ints[4], 'length': ints[5]},
+ 'blue': {'offset': ints[6], 'length': ints[7]},
+ 'green': {'offset': ints[8], 'length': ints[9]},
+ 'alpha': {'offset': ints[10], 'length': ints[11]}}
+ else:
+ return {'size': ints[0], 'width': ints[1], 'height': ints[2]}
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(('127.0.0.1', _ADB_PORT))
+ sendData(sock, 'host:transport:' + targetDevice())
+ ok = readOkay(sock)
+ if not ok:
+ return None, None
+ sendData(sock, 'framebuffer:')
+ if readOkay(sock):
+ version = struct.unpack('@I', readData(sock, 4))[0] # ntohl
+ if version == 16: # compatibility mode
+ headerFields = 3 # size, width, height
+ else:
+ headerFields = 12 # bpp, size, width, height, 4*(offset, length)
+ header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4)))
+ sendData(sock, '\x00')
+ data = readData(sock)
+ result = ""
+ while len(data):
+ result += data
+ data = readData(sock)
+ sock.close()
+ return header, result # pass size returned in header
+ else:
+ sock.close()
+ return None, None
+
+def captureScreen(localFileName, boundary):
+ header, data = framebuffer()
+ width = header['width']
+ height = header['height']
+ dimensions = (width, height)
+ if header['bpp'] == 32:
+ components = {header['red']['offset']: 'R',
+ header['green']['offset']: 'G',
+ header['blue']['offset']: 'B'}
+ alpha = header['alpha']['length'] != 0
+ if alpha:
+ components[header['alpha']['offset']] = 'A'
+ format = '' + components[0] + components[8] + components[16]
+ if alpha:
+ format += components[24]
+ image = Image.fromstring('RGBA', dimensions, data, 'raw', format)
+ else:
+ image = Image.fromstring('RGBA', dimensions, data)
+ r, g, b, a = image.split()
+ image = Image.merge('RGB', (r, g, b))
+ else: # assuming BGR565
+ image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16')
+ image = image.crop(boundary)
+ image.save(localFileName, optimize=1)
+
+def waitForReply(type):
+ cmd = ' logcat ' + _LOG_FILTER + ':V *:S'
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT)
+
+ while True:
+ line = proc.stdout.readline()
+
+ if re.match(r'^[A-Z]/' + _LOG_FILTER, line):
+ line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line)
+ line = re.sub(r'Console: ', '', line)
+ line = re.sub(r':(\d)+(\b)*', '', line)
+ line = re.sub(r'\r\n', '', line)
+
+ if (line.startswith("#")):
+ print line
+ continue
+
+ try:
+ reply = eval(line)
+ except Exception as e:
+ setExitCode(ExitCode.Aborted)
+ setError('Error in protocol: unrecognized message "' + line + '"')
+ raise e
+
+ error = reply['error']
+ if error:
+ setExitCode(ExitCode.Aborted)
+ setError(error)
+ raise Exception()
+
+ if reply['type'] == _REPLY_SCREEN:
+ if not _g_isPilAvailable:
+ setExitCode(ExitCode.Aborted)
+ setError('Screen capture requested but Python Imaging Library (PIL) not found.')
+ raise Exception()
+
+ _g_state['screenCaptureCount'] += 1
+ localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png'
+ boundary = (reply['boundaryLeft'], reply['boundaryTop'],
+ reply['boundaryRight'], reply['boundaryBottom'])
+ captureScreen(localFileName, boundary)
+ sendIntent(_INTENT_SCREEN_DONE)
+
+ elif reply['type'] == type:
+ proc.kill()
+ clearLogcat()
+ return reply
+
+def printUsage():
+ app = os.path.basename(sys.argv[0])
+ print "Usage: ", app, "\t\t- assume one attached device only"
+ print " ", app, _OPTION_DEVICE, "<id>\t- connect to device with serial number <id>"
+ print " ", app, _OPTION_HELP, "\t\t- print this help"
+
+def readData(socket, max = 4096):
+ return socket.recv(max)
+
+def readOkay(socket):
+ data = socket.recv(4)
+ return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y'
+
+def sendData(socket, str):
+ return socket.sendall('%04X%s' % (len(str), str))
+
+def execute(cmd):
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ proc.stdin.close()
+ proc.wait()
+
+def startAdbServer():
+ execute('start-server')
+
+def query(cmd):
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ output = proc.stdout.read()
+ proc.stdin.close()
+ proc.wait()
+ return output
+
+def devices():
+ sock = startConnection(_ADB_PORT)
+ sendData(sock, 'host:devices')
+ if readOkay(sock):
+ readData(sock, 4) # payload size in hex
+ data = readData(sock)
+ reply = ""
+ while len(data):
+ reply += data
+ data = readData(sock)
+ sock.close()
+ devices = re.sub('List of devices attached\s+', '', reply)
+ devices = devices.splitlines()
+ list = []
+ for elem in devices:
+ if elem.find('device') != -1:
+ list.append(re.sub(r'\s*device', '', elem))
+ return list
+ else: # adb server not running
+ sock.close()
+ return None
+
+def isAvailable():
+ return query('version').startswith('Android Debug Bridge')
+
+def sendIntent(intent, package=_TARGET_PACKAGE, data=''):
+ clearLogcat()
+ cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY
+ if data:
+ cmd += " -d '" + data + "'"
+ execute(cmd)
+
+def pull(remote, local):
+ execute('pull ' + remote + ' ' + local)
+
+def push(local, remote):
+ execute('push ' + local + ' ' + remote)
+
+def runTest():
+ def checkError(r):
+ error = r['error']
+ if error:
+ setExitCode(ExitCode.Aborted)
+ setError(error)
+ raise Exception()
+
+ print "Launching remote application..."
+ sendIntent(_INTENT_VIEW, _STANDARD_PACKAGE)
+ reply = waitForReply(_REPLY_READY)
+ checkError(reply)
+
+ print "Sending playback events..."
+ sendIntent(_INTENT_PLAY)
+ reply = waitForReply(_REPLY_EVENTS_PATH)
+ file = tempfile.NamedTemporaryFile()
+ file.write(_g_events)
+ file.flush()
+
+ push(file.name, reply["value"])
+ file.close()
+ sendIntent(_INTENT_PUSH_DONE)
+
+ print "Playing test..."
+ reply = waitForReply(_REPLY_DONE)
+ checkError(reply)
+
+ prefix = reply['filesPath']
+ consoleLogFile = reply['consoleLogFile']
+
+ print "Fetching results..."
+ pull(remote=(prefix+'/'+consoleLogFile), local=_CONSOLE_LOG_FILE_NAME)
+
+ print "Done."
+
+def main():
+ args = sys.argv[1:]
+
+ if _OPTION_HELP in args:
+ printUsage()
+ return ExitCode.Help
+
+ if not isAvailable():
+ print "'adb' not found, please add its location to $PATH."
+ return ExitCode.AdbNotFound
+
+ startAdbServer()
+ deviceList = devices()
+
+ if len(deviceList) == 0:
+ print "No attached devices."
+ return ExitCode.NoDevices
+
+ if _OPTION_DEVICE in args:
+ try:
+ serial = args[args.index(_OPTION_DEVICE) + 1]
+ except IndexError:
+ print "Must specify a device serial number."
+ return ExitCode.WrongUsage
+ if serial in deviceList:
+ setTargetDevice(serial)
+ else:
+ print "Device " + serial + " not found."
+ return ExitCode.UnknownDevice
+ else:
+ if len(deviceList) > 1:
+ print "Multiple devices attached, one must be specified."
+ return ExitCode.MultipleDevices
+
+ print "EventRecorder - Remote Automated Web Application Testing for Android."
+ if not targetDevice():
+ setTargetDevice(deviceList[0])
+
+ print "Target device is " + targetDevice() + "."
+
+ try:
+ runTest()
+ except Exception as e:
+ print e
+ code = exitCode()
+ if code == ExitCode.Normal:
+ print "Exiting..."
+ elif code == ExitCode.DeviceDisconnected:
+ print "Device disconnected."
+ elif code == ExitCode.Aborted:
+ print _g_state['error']
+ return code
+
+if __name__ == "__main__":
+ sys.exit(main())
View
4 eventrecorder/src/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">EventRecorder</string>
+</resources>
View
677 eventrecorder/src/android/src/com/sencha/eventrecorder/App.java
@@ -0,0 +1,677 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.SyncFailedException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.StringTokenizer;
+
+public class App extends Activity {
+
+ static final public String APPLICATION = "EventRecorder";
+ static final public String COMMAND_JAVASCRIPT = "javascript";
+ static final public String COMMAND_KEY = "key";
+ static final public String COMMAND_PAUSE = "pause";
+ static final public String COMMAND_SCREEN = "screen";
+ static final public String COMMAND_TOUCH = "touch";
+ static final public String COMMAND_TEXT = "text";
+ static final public String COMMAND_URL = "url";
+ static final public String CONSOLE_LOG = "console.log";
+ static final public String EVENT_INPUT_FILE = "events.txt";
+ static final public String INTENT_CLEANUP = "CLEANUP";
+ static final public String INTENT_JAVASCRIPT = "JAVASCRIPT";
+ static final public String INTENT_PLAY = "PLAY";
+ static final public String INTENT_PUSH_DONE = "PUSH_DONE";
+ static final public String INTENT_RECORD = "RECORD";
+ static final public String INTENT_SCREEN = "SCREEN";
+ static final public String INTENT_SCREEN_DONE = "SCREEN_DONE";
+ static final public String INTENT_STOP = "STOP";
+ static final public String INTENT_TEXT_INPUT = "TEXT_INPUT";
+ static final public String INTENT_URL = "URL";
+ static final public String INTENT_VIEW = "VIEW";
+ static final public String LOGTAG = APPLICATION;
+ static final public String PLAYBACK_FILE= "run.py";
+
+ static private Activity sInstance = null;
+
+ static public Activity getSingletonInstance() {
+ return sInstance;
+ }
+
+ public Rect viewPosition() {
+ View v = getWindow().findViewById(Window.ID_ANDROID_CONTENT);
+ return new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+ }
+
+ class MyWebView extends WebView {
+ public long mUrlTime;
+
+ private EventWriter mTemplate;
+ private MotionEvent mLastMotionEvent;
+ private boolean mBlockEvents;
+ private long mStartTime;
+
+ MyWebView(Context context) {
+ super(context);
+
+ mBlockEvents = true;
+ mLastMotionEvent = null;
+ mUrlTime = 0;
+ }
+
+ public boolean dispatchKeyEventOverride(KeyEvent event) {
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mTemplate != null) {
+ long time = MyWebView.this.getTimeDelta(event.getEventTime());
+ int action = event.getAction();
+ int repeat = event.getRepeatCount();
+ int meta = event.getMetaState();
+ int device = event.getDeviceId();
+ int code = event.getKeyCode();
+ int scan = event.getScanCode();
+ int flags = event.getFlags();
+ String chars = event.getCharacters();
+
+ mTemplate.writeKeyEvent(time, code, action, repeat, meta, device, scan, flags, chars);
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mBlockEvents)
+ return true;
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ event = MotionEvent.obtainNoHistory(event);
+ if (mLastMotionEvent != null) {
+ int dx = (int)(mLastMotionEvent.getX(0) - event.getX(0));
+ int dy = (int)(mLastMotionEvent.getY(0) - event.getY(0));
+ if (dx == 0 && dy == 0)
+ return true;
+ }
+ mLastMotionEvent = event;
+ }
+
+ final int historySize = event.getHistorySize();
+ final float xPrecision = event.getXPrecision();
+ final float yPrecision = event.getYPrecision();
+ final int deviceId = event.getDeviceId();
+ final int meta = event.getMetaState();
+ final int edge = event.getEdgeFlags();
+
+ for (int h = 0; h < historySize; ++h) {
+ if (mTemplate != null)
+ mTemplate.writeTouchEvent(getTimeDelta(event.getHistoricalEventTime(h)),
+ event.getAction(),
+ event.getHistoricalX(0, h),
+ event.getHistoricalY(0, h),
+ xPrecision, yPrecision,
+ event.getHistoricalPressure(0, h),
+ event.getHistoricalSize(0, h),
+ deviceId, meta, edge);
+ }
+
+ if (mTemplate != null)
+ mTemplate.writeTouchEvent(getTimeDelta(event.getEventTime()),
+ event.getAction(),
+ event.getX(0), event.getY(0),
+ xPrecision, yPrecision,
+ event.getPressure(0),
+ event.getSize(0),
+ deviceId, meta, edge);
+
+ try {
+ return super.onTouchEvent(event);
+ } catch (NullPointerException e) {
+ return true; // Necessary to work around a possible application crash with unknown cause.
+ }
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ return super.onTrackballEvent(event);
+ }
+
+ public void startRecording() {
+ mStartTime = SystemClock.uptimeMillis();
+ mTemplate = new EventWriter(Storage.getAbsoluteFilePath("run.py"));
+ if (mTemplate != null) {
+ mTemplate.writeHeader();
+ } else {
+ Log.i(LOGTAG, Reply.error("Unable to create output playback file"));
+ }
+
+ App.this.initConsoleLogFile();
+ }
+
+ public void pause() {
+ if (mTemplate != null)
+ mTemplate.writeCommand(getTimeDelta(SystemClock.uptimeMillis()), "pause");
+ }
+
+ public void stopRecording() {
+ if (mTemplate != null) {
+ mTemplate.writeFooter();
+ mTemplate.close();
+ mTemplate = null;
+ }
+ App.this.deinitConsoleLogFile();
+ mUrlTime = 0;
+ }
+
+ public void startCaptureScreen() {
+ if (mTemplate != null)
+ mTemplate.writeCommand(getTimeDelta(SystemClock.uptimeMillis()), "screen");
+
+ Log.i(LOGTAG, Reply.screen(App.this.viewPosition()));
+ }
+
+ public void openUrl(String url) {
+ if (mTemplate != null)
+ mTemplate.writeCommand(getTimeDelta(SystemClock.uptimeMillis()), COMMAND_URL, url);
+ this.loadUrl(url);
+ }
+
+ public void evaluateScript(String script) {
+ if (mTemplate != null)
+ mTemplate.writeCommand(getTimeDelta(SystemClock.uptimeMillis()), COMMAND_JAVASCRIPT, script);
+
+ this.loadUrl(script);
+ }
+
+ public void setBlockMotionEvents(boolean block) {
+ if (!block)
+ mLastMotionEvent = null;
+ mBlockEvents = block;
+ }
+
+ public boolean motionEventsBlocked() {
+ return mBlockEvents;
+ }
+
+ public void injectText(long timestamp, String text) {
+ KeyEvent event = new KeyEvent(timestamp, text, 0, 0);
+ dispatchKeyEventOverride(event);
+
+ mTemplate.writeTextEvent(getTimeDelta(timestamp), text);
+
+ InputMethodManager imm = (InputMethodManager)App.this.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+
+ private long getTimeDelta(long now) {
+ if (mUrlTime == 0)
+ return 0;
+ long ret = now - mUrlTime;
+ return ret;
+ }
+ }
+
+ private MyWebView mWebView;
+
+ private String base64Decode(String base64) {
+ if (base64 != null) {
+ try {
+ return Base64.decode(base64);
+ } catch (ParseException e) {
+ }
+ }
+ return null;
+ }
+
+ protected void onNewIntent(Intent intent) {
+ String action = intent.getAction();
+ String data = intent.getDataString();
+ String pack = getPackageName() + ".";
+
+ if (action.equals(pack + INTENT_RECORD)) {
+ mWebView.scrollTo(0, 0);
+ mWebView.startRecording();
+ Log.i(LOGTAG, Reply.ready());
+
+ } else if (action.equals(pack + INTENT_STOP)) {
+ mWebView.stopRecording();
+ String done = Reply.done(Storage.getDirectory());
+ Log.i(LOGTAG, done);
+
+ } else if (action.equals(pack + INTENT_PLAY)) {
+ mWebView.scrollTo(0, 0);
+ String eventsFilePath = Storage.getAbsoluteFilePath(EVENT_INPUT_FILE);
+ Log.i(LOGTAG, Reply.eventsFilePath(eventsFilePath));
+
+ } else if (action.equals(pack + INTENT_PUSH_DONE)) {
+ startPlayingEventData();
+
+ } else if (action.equals(pack + INTENT_CLEANUP)) {
+ cleanup();
+
+ } else if (action.equals(pack + INTENT_SCREEN)) {
+ mWebView.startCaptureScreen();
+
+ } else if (action.equals(pack + INTENT_SCREEN_DONE)) {
+ if (mIsPlaying) {
+ mListRunner.resume();
+ }
+
+ } else if (action.equals(pack + INTENT_URL) && data != null) {
+ mWebView.openUrl(base64Decode(data));
+
+ } else if (action.equals(pack + INTENT_JAVASCRIPT) && data != null) {
+ data = base64Decode(data);
+ if (!data.startsWith("javascript:"))
+ data = "javascript:" + data;
+ mWebView.evaluateScript(data);
+
+ } else if (action.equals(pack + INTENT_TEXT_INPUT) && data != null) {
+ mWebView.injectText(SystemClock.uptimeMillis(), base64Decode(data));
+
+ } else if (action.equals("android.intent.action." + INTENT_VIEW))
+ Log.i(LOGTAG, Reply.ready());
+ }
+
+ class ListRunner {
+ class Event {
+ Event() {
+ commands = new ArrayList<String>();
+ object = null;
+ }
+
+ public long timestamp;
+ public String action;
+ public ArrayList<String> commands;
+
+ public Object object;
+ }
+
+ private ArrayList<Event> mEvents;
+
+ ListRunner() {
+ mTimer = new HandlerTimer();
+ mDone = true;
+ mFilesPath = null;
+ }
+
+ public void start(String lines) {
+ mDone = false;
+ mFilesPath = Storage.getDirectory();
+ mEvents = new ArrayList<Event>();
+ parseEvents(lines);
+ scheduleUntilPause();
+ }
+
+ private void parseEvents(String lines) {
+ StringTokenizer at = new StringTokenizer(lines, "\n");
+ while (at.hasMoreTokens()) {
+ StringTokenizer lt = new StringTokenizer(at.nextToken());
+ int count = 0;
+
+ Event event = new Event();
+
+ while (lt.hasMoreTokens()) {
+ switch (count) {
+ case 0:
+ event.timestamp = Long.decode(lt.nextToken());
+ break;
+ case 1:
+ event.action = lt.nextToken();
+ break;
+ default:
+ event.commands.add(lt.nextToken());
+ break;
+ }
+ ++count;
+ }
+
+ mEvents.add(event);
+ }
+ }
+
+ public void resume() {
+ scheduleUntilPause();
+ }
+
+ private void sendDone(long base) {
+ mTimer.scheduleAt(new Runnable() {
+ public void run() {
+ mWebView.stopRecording();
+ App.this.mIsPlaying = false;
+ Log.i(LOGTAG, Reply.done(mFilesPath));
+ }
+ }, 5000 + base);
+ }
+
+ abstract class EventRunnable implements Runnable {
+ EventRunnable(Event event) {
+ mEvent = event;
+ }
+
+ protected Event mEvent;
+ }
+
+ private void scheduleUntilPause() {
+ long scheduleTime = 0;
+ long urlTime = App.this.mWebView.mUrlTime;
+ boolean paused = false;
+
+ while (!mEvents.isEmpty()) {
+ Event currentEvent = mEvents.remove(0);
+ scheduleTime = currentEvent.timestamp;
+
+ String command = currentEvent.action;
+ if (command.equals(COMMAND_TOUCH)) {
+ parseTouch(currentEvent, scheduleTime + urlTime);
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ App.this.mWebView.onTouchEvent((MotionEvent)mEvent.object);
+ }
+ }, scheduleTime + urlTime);
+
+ } else if (command.equals(COMMAND_KEY)) {
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ handleKey(mEvent);
+ }
+ }, scheduleTime + urlTime);
+
+ } else if (command.equals(COMMAND_SCREEN)) {
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ mWebView.startCaptureScreen();
+ }
+ }, scheduleTime + urlTime);
+
+ paused = true; // Screen also implies pause.
+ break;
+
+ } else if (command.equals(COMMAND_TEXT)) {
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ String text = new String();
+ final int sz = mEvent.commands.size();
+ for (int i = 0; i < sz; ++i) {
+ text += mEvent.commands.get(i);
+ if (i + 1 < sz)
+ text += " ";
+ }
+ mWebView.injectText(SystemClock.uptimeMillis(), text);
+ }
+ }, scheduleTime + urlTime);
+
+ } else if (command.equals(COMMAND_URL)) {
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ if (!mEvent.commands.isEmpty())
+ mWebView.openUrl(mEvent.commands.get(0));
+ }
+ }, scheduleTime + urlTime);
+
+ } else if (command.equals(COMMAND_JAVASCRIPT)) {
+ mTimer.scheduleAt(new EventRunnable(currentEvent) {
+ public void run() {
+ if (!mEvent.commands.isEmpty()) {
+ mWebView.evaluateScript(mEvent.commands.get(0));
+ }
+ }
+ }, scheduleTime + urlTime);
+
+ } else if (command.equals(COMMAND_PAUSE)) {
+ paused = true;
+ break;
+ }
+ }
+
+ if (!paused && mEvents.isEmpty()) {
+ if (!mDone) {
+ mDone = true;
+ sendDone(scheduleTime + urlTime);
+ }
+ }
+ }
+
+ private boolean handleKey(Event event) {
+ int code = 0, action = 0, repeat = 0, meta = 0, device = 0, scan = 0, flags = 0;
+ String chars = null;
+
+ if (event.commands.size() < 7) {
+ Log.i(LOGTAG, Reply.error("Insufficient arguments while parsing key event"));
+ return false;
+ }
+
+ code = Integer.decode(event.commands.get(0));
+ action = Integer.decode(event.commands.get(1));
+ repeat = Integer.decode(event.commands.get(2));
+ meta = Integer.decode(event.commands.get(3));
+ device = Integer.decode(event.commands.get(4));
+ scan = Integer.decode(event.commands.get(5));
+ flags = Integer.decode(event.commands.get(6));
+
+ if (event.commands.size() > 7) {
+ chars = new String();
+ boolean cont;
+ for (int i = 7; i < event.commands.size(); ++i) {
+ chars += event.commands.get(i);
+ if (i + i < event.commands.size())
+ chars += " ";
+ }
+ }
+
+ long time = SystemClock.uptimeMillis();
+ KeyEvent keyEvent;
+ if (chars != null)
+ // FIXME: Call KeyEvent.changeAction here?
+ keyEvent = new KeyEvent(time, chars, device, flags);
+ else
+ keyEvent = new KeyEvent(time, time, action, code, repeat, meta, device, scan, flags);
+
+ App.this.mWebView.dispatchKeyEvent(keyEvent);
+
+ return true;
+ }
+
+ private boolean parseTouch(Event event, long time) {
+ int action = 0;
+ float x = 0, y = 0;
+ float xPrecision = 0, yPrecision = 0;
+ float size = 0, pressure = 0;
+ int deviceId = 0, meta = 0, edge = 0;
+
+ if (event.commands.size() < 10) {
+ Log.i(LOGTAG, Reply.error("Insufficient arguments while parsing touch event"));
+ return false;
+ }
+
+ action = Integer.decode(event.commands.get(0));
+ x = Float.parseFloat(event.commands.get(1));
+ y = Float.parseFloat(event.commands.get(2));
+ xPrecision = Float.parseFloat(event.commands.get(3));
+ yPrecision = Float.parseFloat(event.commands.get(4));
+ pressure = Float.parseFloat(event.commands.get(5));
+ size = Float.parseFloat(event.commands.get(6));
+ deviceId = Integer.decode(event.commands.get(7));
+ meta = Integer.decode(event.commands.get(8));
+ edge = Integer.decode(event.commands.get(9));
+
+ event.object = MotionEvent.obtain(mTouchDownTime, time, action, x, y, pressure, size, meta, xPrecision, yPrecision, deviceId, edge);
+ return true;
+ }
+
+ private HandlerTimer mTimer;
+ private Event mCurrentEvent;
+ private boolean mDone;
+ private String mFilesPath;
+ private long mTouchDownTime;
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ sInstance = this;
+
+ mIsPlaying = false;
+ mPaused = false;
+ mTimer = new HandlerTimer();
+ mListRunner = new ListRunner();
+
+ createWebView();
+ Log.i(LOGTAG, Reply.ready());
+ }
+
+ public void startPlayingEventData() {
+ mWebView.clearCache(true);
+ mWebView.clearFormData();
+ FileAccess eventFile = new FileAccess(Storage.getAbsoluteFilePath(EVENT_INPUT_FILE), "r");
+ String eventData = eventFile.readAll();
+ if (eventData != null) {
+ mWebView.startRecording();
+ mListRunner.start(eventData);
+ mIsPlaying = true;
+ }
+ }
+
+ public void cleanup() {
+ File path = new File(Storage.getDirectory());
+ File[] files = path.listFiles();
+ for (int i = 0; i < files.length; ++i) {
+ if (files[i].getAbsolutePath().contains(getPackageName())) // Sanity check.
+ files[i].delete();
+ }
+ }
+
+ public void initConsoleLogFile() {
+ mJavaScriptFile = new FileAccess(Storage.getAbsoluteFilePath(CONSOLE_LOG), "w");
+ }
+
+ public void deinitConsoleLogFile() {
+ if (mJavaScriptFile != null) {
+ try {
+ mJavaScriptFile.getFileDescriptor().sync();
+ } catch (SyncFailedException e) {
+ }
+ mJavaScriptFile.close();
+ mJavaScriptFile = null;
+ }
+ }
+
+ private void createWebView() {
+ mWebView = new MyWebView(this);
+
+ mWebView.getSettings().setDomStorageEnabled(true);
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
+ mWebView.getSettings().setSaveFormData(false);
+ mWebView.getSettings().setUseWideViewPort(true);
+
+ mWebView.setWebChromeClient(new WebChromeClient() {
+ public void onConsoleMessage(String message, int lineNumber, String sourceID) {
+ if (mJavaScriptFile != null) {
+ mJavaScriptFile.writeText(message + "\n");
+ }
+ }
+
+ public void onProgressChanged(WebView view, int percent) {
+ if (view != mWebView)
+ return;
+
+ if (percent < 100) {
+ if (!mPaused) {
+ mPaused = true;
+ mWebView.pause();
+ }
+
+ if (!mWebView.motionEventsBlocked())
+ mWebView.setBlockMotionEvents(true);
+
+ App.this.setTitle(APPLICATION + " [" + percent + "%]");
+
+ if (mUrlDoneTimer != null) {
+ mTimer.cancel(mUrlDoneTimer);
+ mUrlDoneTimer = null;
+ }
+ } else {
+ mUrlDoneTimer = new Runnable() {
+ public void run() {
+ App.this.setTitle(APPLICATION + " [Loaded]");
+
+ mWebView.setBlockMotionEvents(false);
+
+ mPaused = false;
+ mWebView.mUrlTime = SystemClock.uptimeMillis();
+ if (App.this.mIsPlaying)
+ mListRunner.resume();
+ mUrlDoneTimer = null;
+ }
+ };
+
+ mTimer.schedule(mUrlDoneTimer, 1000);
+ }
+ }
+ });
+
+ mWebView.setWebViewClient(new WebViewClient() {
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return true;
+ }
+ });
+
+ setContentView(mWebView);
+ }
+
+ private FileAccess mJavaScriptFile;
+ private HandlerTimer mTimer;
+ private Runnable mUrlDoneTimer;
+ private ListRunner mListRunner;
+ private boolean mIsPlaying;
+ private boolean mPaused;
+}
View
76 eventrecorder/src/android/src/com/sencha/eventrecorder/Base64.java
@@ -0,0 +1,76 @@
+/*
+Copyright (c) 2010 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import java.text.ParseException;
+
+public class Base64 {
+ public static String encode(String input) {
+ return "";
+ }
+
+ public static String decode(String input) throws ParseException {
+ int len = ((input.length() + 3) / 4) * 3;
+ byte[] result = new byte[len];
+ int pos = -1;
+
+ char c;
+ int temp = 0;
+ for (int i = 0; i < input.length(); ++i) {
+ c = input.charAt(i);
+
+ temp <<= 6;
+ if (c >= 'A' && c <= 'Z')
+ temp |= (byte)(c - 'A');
+ else if (c >= 'a' && c <= 'z')
+ temp |= (byte)(c - 'a' + 26);
+ else if (c >= '0' && c <= '9')
+ temp |= (byte)(c - '0' + 52);
+ else if (c == '+')
+ temp |= 62;
+ else if (c == '/')
+ temp |= 63;
+ else if (c == '=') {
+ switch (input.length() - i) {
+ case 1:
+ result[++pos] = (byte)((temp >> 16) & 0xff);
+ result[++pos] = (byte)((temp >> 8) & 0xff);
+ return new String(result, 0, len - 1);
+ case 2:
+ result[++pos] = (byte)((temp >> 10) & 0xff);
+ return new String(result, 0, len - 2);
+ default:
+ throw new ParseException("Invalid number of pad characters", i);
+ }
+ } else {
+ throw new ParseException("Invalid character: " + c, i);
+ }
+ if ((i + 1) % 4 == 0) {
+ result[++pos] = (byte)((temp >> 16) & 0xff);
+ result[++pos] = (byte)((temp >> 8) & 0xff);
+ result[++pos] = (byte)(temp & 0xff);
+ }
+ }
+ return new String(result);
+ }
+}
View
90 eventrecorder/src/android/src/com/sencha/eventrecorder/EventWriter.java
@@ -0,0 +1,90 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+class EventWriter extends PlaybackFileAccess {
+ EventWriter(String fileName) {
+ super(fileName, "w");
+ }
+
+ public void writeCommand(long timestamp, String type, String value) {
+ String text = timestamp + " " + type + " " + escapeQuotes(value) + "\\n";
+ writeText(text);
+ }
+
+ public void writeCommand(long timestamp, String command) {
+ String text = timestamp + " " + command + "\\n";
+ writeText(text);
+ }
+
+ public void writeTouchEvent(long timestamp, int action, float x, float y, float xPrecision, float yPrecision,
+ float pressure, float size, int deviceId, int meta, int edge)
+ {
+ writeCommand(timestamp, App.COMMAND_TOUCH,
+ action
+ + " " + x
+ + " " + y
+ + " " + xPrecision
+ + " " + yPrecision
+ + " " + pressure
+ + " " + size
+ + " " + deviceId
+ + " " + meta
+ + " " + edge);
+ }
+
+ public void writeKeyEvent(long timestamp, int code, int action, int repeat, int meta,
+ int device, int scan, int flags, String chars)
+ {
+ String event = code
+ + " " + action
+ + " " + repeat
+ + " " + meta
+ + " " + device
+ + " " + scan
+ + " " + flags;
+
+ if (chars != null)
+ event += " " + escapeNewlines(chars);
+ writeCommand(timestamp, App.COMMAND_KEY, event);
+ }
+
+ public void writeTextEvent(long timestamp, String text) {
+ writeCommand(timestamp, App.COMMAND_TEXT, text);
+ }
+
+ private String escapeQuotes(String text)
+ {
+ String newtext = text.replace("\"", "\\\"");
+ return newtext.replace("'", "\\'");
+ }
+
+ private String escapeNewlines(String text)
+ {
+ return text.replace("\n", "\\\n");
+ }
+
+}
View
108 eventrecorder/src/android/src/com/sencha/eventrecorder/FileAccess.java
@@ -0,0 +1,108 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.util.Log;
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+class FileAccess {
+ FileAccess(String fileName, String mode) {
+ try {
+ File file = new File(fileName);
+ if (mode.equals("w")) {
+ file.delete();
+ mode = "rw";
+ }
+ mFile = new RandomAccessFile(file, mode);
+ mFileName = fileName;
+ } catch (IOException e) {
+ Log.i(App.LOGTAG, Reply.error("Unable to create file " + fileName));
+ }
+ }
+
+ public String getFileName() {
+ return mFileName;
+ }
+
+ public FileDescriptor getFileDescriptor() {
+ try {
+ return mFile.getFD();
+ } catch (IOException e) {
+ }
+ return null;
+ }
+
+ public String readAll() {
+ try {
+ return readAll(new FileInputStream(mFile.getFD()));
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ protected String readAll(InputStream reader) {
+ try {
+ String data = new String();
+
+ byte[] buf = new byte[4096];
+ int rd;
+ do {
+ rd = reader.read(buf, 0, 4096);
+ if (rd > 0)
+ data += new String(buf, 0, rd);
+ } while (rd != -1);
+
+ return data;
+ } catch (IOException e) {
+ Log.i(App.LOGTAG, Reply.error("Error when reading file " + mFileName));
+ }
+
+ return null;
+ }
+
+ public void writeText(String text) {
+ try {
+ mFile.writeBytes(text);
+ } catch (IOException e) {
+ Log.i(App.LOGTAG, Reply.error("Unable to write to file " + mFileName));
+ }
+ }
+
+ public void close() {
+ try {
+ mFile.close();
+ } catch (IOException e) {
+ Log.i(App.LOGTAG, Reply.error("Unable to close file " + mFileName));
+ }
+ }
+
+ private RandomAccessFile mFile;
+ private String mFileName;
+}
+
View
46 eventrecorder/src/android/src/com/sencha/eventrecorder/HandlerTimer.java
@@ -0,0 +1,46 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.os.Handler;
+
+class HandlerTimer {
+ HandlerTimer() {
+ if (mHandler == null)
+ mHandler = new Handler();
+ }
+
+ public boolean schedule(Runnable r, long delayMillis) {
+ return mHandler.postDelayed(r, delayMillis);
+ }
+
+ public boolean scheduleAt(Runnable r, long uptimeMillis) {
+ return mHandler.postAtTime(r, uptimeMillis);
+ }
+
+ public void cancel(Runnable r) {
+ mHandler.removeCallbacks(r);
+ }
+
+ private static Handler mHandler = null;
+}
View
47 eventrecorder/src/android/src/com/sencha/eventrecorder/PlaybackFileAccess.java
@@ -0,0 +1,47 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+class PlaybackFileAccess extends FileAccess {
+ PlaybackFileAccess(String fileName, String mode) {
+ super(fileName, mode);
+ InputStream template = App.getSingletonInstance().getResources().openRawResource(R.raw.template);
+ String program = readAll(template);
+ mTemplate = program.split("%EVENTS%");
+ }
+
+ public void writeHeader() {
+ String header = mTemplate[0];
+ writeText(header);
+ }
+
+ public void writeFooter() {
+ String footer = mTemplate[1];
+ writeText(footer);
+ }
+
+ private String[] mTemplate;
+}
View
55 eventrecorder/src/android/src/com/sencha/eventrecorder/Reply.java
@@ -0,0 +1,55 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.graphics.Rect;
+
+class Reply {
+ static public String error(String error) {
+ return "{'error': '" + error + "'}";
+ }
+
+ static public String eventsFilePath(String path) {
+ return "{'type': 'eventsFilePath', 'value': '" + path + "', 'error': ''}";
+ }
+
+ static public String ready() {
+ return "{'type': 'ready', 'error': ''}";
+ }
+
+ static public String screen(Rect boundary) {
+ return "{'type': 'screen', 'error': ''"
+ + ", 'boundaryLeft': " + boundary.left
+ + ", 'boundaryTop': " + boundary.top
+ + ", 'boundaryRight': " + boundary.right
+ + ", 'boundaryBottom': " + boundary.bottom
+ + "}";
+ }
+
+ static public String done(String filesPath) {
+ return "{'type': 'done', 'error': '', "
+ + "'filesPath': '" + filesPath + "', "
+ + "'consoleLogFile': '" + App.CONSOLE_LOG + "', "
+ + "'testScriptFile': '" + App.PLAYBACK_FILE + "'}";
+ }
+}
View
44 eventrecorder/src/android/src/com/sencha/eventrecorder/Storage.java
@@ -0,0 +1,44 @@
+/*
+Copyright (c) 2011 Sencha Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+package com.sencha.eventrecorder;
+
+import android.os.Environment;
+import java.io.File;
+
+class Storage {
+ static public String getDirectory() {
+ File path;
+ String state = Environment.getExternalStorageState();
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ path = new File(Environment.getExternalStorageDirectory(),
+ "Android/data/" + App.getSingletonInstance().getPackageName());
+ path.mkdirs();
+ } else
+ path = App.getSingletonInstance().getCacheDir();
+ return path.getAbsolutePath();
+ }
+
+ static public String getAbsoluteFilePath(String fileName) {
+ return getDirectory() + "/" + fileName;
+ }
+}
View
BIN  eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/carousel/droidincredible/froyo/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/forms/nexusone/froyo/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/forms/nexusone/gingerbread/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/icons/evo4g/froyo/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/icons/evo4g/froyo/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/overlays/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/overlays/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/overlays/screen3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/overlays/screen4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
2  eventrecorder/src/examples/results/picker/nexusone/gingerbread/console.log
@@ -0,0 +1,2 @@
+Mon Jun 27 1988 00:00:00 GMT-0700 (PDT)
+Mon Jun 27 1988 00:00:00 GMT-0700 (PDT)
View
BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/picker/nexusone/gingerbread/screen3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/evo4g/froyo/screen5.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  eventrecorder/src/examples/results/tabs2/nexusone/froyo/screen5.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
385 eventrecorder/src/examples/tests/carousel.py
@@ -0,0 +1,385 @@
+#!/usr/bin/env python
+
+_g_isPilAvailable = False
+try:
+ from PIL import Image
+ _g_isPilAvailable = True
+except:
+ pass
+
+from subprocess import Popen, PIPE, STDOUT
+import base64
+import math
+import os
+import re
+import socket
+import struct
+import sys
+import tempfile
+import thread
+import time
+
+_OPTION_DEVICE = "-s"
+_OPTION_HELP = "-h"
+
+_TARGET_PACKAGE = 'com.sencha.eventrecorder'
+_TARGET_ACTIVITY = _TARGET_PACKAGE + '/.App'
+_STANDARD_PACKAGE = 'android.intent.action'
+
+_ADB_PORT = 5037
+_LOG_FILTER = 'EventRecorder'
+
+_INTENT_PLAY = "PLAY"
+_INTENT_PUSH_DONE = "PUSH_DONE"
+_INTENT_SCREEN_DONE = "SCREEN_DONE"
+_INTENT_VIEW = "VIEW"
+
+_REPLY_DONE = 'done'
+_REPLY_READY = 'ready'
+_REPLY_SCREEN = 'screen'
+_REPLY_EVENTS_PATH = 'eventsFilePath'
+
+_CONSOLE_LOG_FILE_NAME = "console.log"
+_SCREEN_CAPTURE_PREFIX = "screen"
+_WINDOW_CAPTURE_PREFIX = "window"
+
+class ExitCode:
+ Help = -10
+ Normal = 0
+ AdbNotFound = 5
+ NoDevices = 15
+ DeviceDisconnected = 25
+ MultipleDevices = 35
+ Aborted = 45
+ WrongUsage = 55
+ UnknownDevice = 65
+
+_g_state = {
+ 'exitCode': ExitCode.Normal,
+ 'error': '',
+ 'screenCaptureCount': 0,
+ 'targetDevice': ''
+}
+
+_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'
+
+def exitCode():
+ return _g_state['exitCode']
+
+def setExitCode(err):
+ global _g_state
+ _g_state['exitCode'] = err
+
+def error():
+ return _g_state['error']
+
+def setError(err):
+ global _g_state
+ _g_state['error'] = err
+
+def targetDevice():
+ return _g_state['targetDevice']
+
+def setTargetDevice(id):
+ global _g_state
+ _g_state['targetDevice'] = id
+
+def startConnection(port):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ try:
+ sock.connect(('127.0.0.1', port))
+ return sock
+ except Exception as e:
+ setError('Unable to connect to port %d: %s' % (port, e))
+
+def clearLogcat():
+ cmd = ' logcat -c '
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT)
+ time.sleep(1)
+ proc.kill()
+
+def framebuffer():
+ def headerMap(ints):
+ if len(ints) == 12:
+ return {'bpp': ints[0], 'size': ints[1], 'width': ints[2], 'height': ints[3],
+ 'red': {'offset': ints[4], 'length': ints[5]},
+ 'blue': {'offset': ints[6], 'length': ints[7]},
+ 'green': {'offset': ints[8], 'length': ints[9]},
+ 'alpha': {'offset': ints[10], 'length': ints[11]}}
+ else:
+ return {'size': ints[0], 'width': ints[1], 'height': ints[2]}
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(('127.0.0.1', _ADB_PORT))
+ sendData(sock, 'host:transport:' + targetDevice())
+ ok = readOkay(sock)
+ if not ok:
+ return None, None
+ sendData(sock, 'framebuffer:')
+ if readOkay(sock):
+ version = struct.unpack('@I', readData(sock, 4))[0] # ntohl
+ if version == 16: # compatibility mode
+ headerFields = 3 # size, width, height
+ else:
+ headerFields = 12 # bpp, size, width, height, 4*(offset, length)
+ header = headerMap(struct.unpack('@IIIIIIIIIIII', readData(sock, headerFields * 4)))
+ sendData(sock, '\x00')
+ data = readData(sock)
+ result = ""
+ while len(data):
+ result += data
+ data = readData(sock)
+ sock.close()
+ return header, result # pass size returned in header
+ else:
+ sock.close()
+ return None, None
+
+def captureScreen(localFileName, skipLines = 0):
+ header, data = framebuffer()
+ width = header['width']
+ height = header['height']
+ dimensions = (width, height)
+ if header['bpp'] == 32:
+ components = {header['red']['offset']: 'R',
+ header['green']['offset']: 'G',
+ header['blue']['offset']: 'B'}
+ alpha = header['alpha']['length'] != 0
+ if alpha:
+ components[header['alpha']['offset']] = 'A'
+ format = '' + components[0] + components[8] + components[16]
+ if alpha:
+ format += components[24]
+ image = Image.fromstring('RGBA', dimensions, data, 'raw', format)
+ else:
+ image = Image.fromstring('RGBA', dimensions, data)
+ r, g, b, a = image.split()
+ image = Image.merge('RGB', (r, g, b))
+ else: # assuming BGR565
+ image = Image.fromstring('RGB', dimensions, data, 'raw', 'BGR;16')
+ image = image.crop((0, skipLines, width - 1, height - 1))
+ image.save(localFileName, optimize=1)
+
+def waitForReply(type):
+ cmd = ' logcat ' + _LOG_FILTER + ':V *:S'
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdout=PIPE, stderr=STDOUT)
+
+ while True:
+ line = proc.stdout.readline()
+
+ if re.match(r'^[A-Z]/' + _LOG_FILTER, line):
+ line = re.sub(r'[A-Z]/' + _LOG_FILTER + '(\b)*\((\s)*(\d)+\): ', '', line)
+ line = re.sub(r'Console: ', '', line)
+ line = re.sub(r':(\d)+(\b)*', '', line)
+ line = re.sub(r'\r\n', '', line)
+
+ if (line.startswith("#")):
+ print line
+ continue
+
+ try:
+ reply = eval(line)
+ except Exception as e:
+ setExitCode(ExitCode.Aborted)
+ setError('Error in protocol: unrecognized message "' + line + '"')
+ raise e
+
+ error = reply['error']
+ if error:
+ setExitCode(ExitCode.Aborted)
+ setError(error)
+ raise Exception()
+
+ if reply['type'] == _REPLY_SCREEN:
+ if not _g_isPilAvailable:
+ setExitCode(ExitCode.Aborted)
+ setError('Screen capture requested but Python Imaging Library (PIL) not found.')
+ raise Exception()
+
+ _g_state['screenCaptureCount'] += 1
+ localFileName = _SCREEN_CAPTURE_PREFIX + `_g_state['screenCaptureCount']` + '.png'
+ skipLines = reply['skipLines']
+ captureScreen(localFileName, skipLines)
+ sendIntent(_INTENT_SCREEN_DONE)
+
+ elif reply['type'] == type:
+ proc.kill()
+ clearLogcat()
+ return reply
+
+def printUsage():
+ app = os.path.basename(sys.argv[0])
+ print "Usage: ", app, "\t\t- assume one attached device only"
+ print " ", app, _OPTION_DEVICE, "<id>\t- connect to device with serial number <id>"
+ print " ", app, _OPTION_HELP, "\t\t- print this help"
+
+def readData(socket, max = 4096):
+ return socket.recv(max)
+
+def readOkay(socket):
+ data = socket.recv(4)
+ return data[0] == 'O' and data[1] == 'K' and data[2] == 'A' and data[3] == 'Y'
+
+def sendData(socket, str):
+ return socket.sendall('%04X%s' % (len(str), str))
+
+def execute(cmd):
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ proc.stdin.close()
+ proc.wait()
+
+def startAdbServer():
+ execute('start-server')
+
+def query(cmd):
+ fullCmd = 'adb '
+ if targetDevice():
+ fullCmd += '-s ' + targetDevice() + ' '
+ fullCmd += cmd
+ proc = Popen(fullCmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ output = proc.stdout.read()
+ proc.stdin.close()
+ proc.wait()
+ return output
+
+def devices():
+ sock = startConnection(_ADB_PORT)
+ sendData(sock, 'host:devices')
+ if readOkay(sock):
+ readData(sock, 4) # payload size in hex
+ data = readData(sock)
+ reply = ""
+ while len(data):
+ reply += data
+ data = readData(sock)
+ sock.close()
+ devices = re.sub('List of devices attached\s+', '', reply)
+ devices = devices.splitlines()
+ list = []
+ for elem in devices:
+ if elem.find('device') != -1:
+ list.append(re.sub(r'\s*device', '', elem))
+ return list
+ else: # adb server not running
+ sock.close()
+ return None
+
+def isAvailable():
+ return query('version').startswith('Android Debug Bridge')
+
+def sendIntent(intent, package=_TARGET_PACKAGE, data=''):
+ clearLogcat()
+ cmd = 'shell am start -a ' + package + '.' + intent + ' -n ' + _TARGET_ACTIVITY
+ if data:
+ cmd += " -d '" + data + "'"
+ execute(cmd)
+
+def pull(remote, local):
+ execute('pull ' + remote + ' ' + local)
+
+def push(local, remote):
+ execute('push ' + local + ' ' + remote)
+
+def runTest():
+ def checkError(r):
+ error = r['error']
+ if error:
+ setExitCode(ExitCode.Aborted)
+ setError(error)
+ raise Exception()
+
+ print "Launching remote application..."