/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.unit;
import totalcross.io.*;
import totalcross.sys.*;
import totalcross.ui.*;
import totalcross.ui.dialog.*;
import totalcross.ui.event.*;
import totalcross.ui.gfx.*;
import totalcross.ui.image.*;
import totalcross.util.*;
import totalcross.util.Comparable;
import totalcross.util.concurrent.*;
/** This class permits the control of the User Interface, playing back events recorded
* by the user.
*
* The robot is comprised of some dialogs that are invoked using a special key defined by the application.
* The special key running in Java SE (eclipse, netbeans) is control+1. The code below defines the
* Find special key to be used at the device (you can choose any special key you want):
* <pre>
* Vm.interceptSpecialKeys(new int[]{SpecialKeys.FIND});
* Settings.deviceRobotSpecialKey = SpecialKeys.FIND;
* </pre>
* You should set it at the application's constructor.
*
* When this key is pressed, it opens a window with three options: record, playback and cancel.
*
* Clicking on the "record" button opens another window asking for the robot's name. Type the name
* and press "Start record" to start recording. Do the events smoothly and slowly. When done, press the special
* key again.
*
* Clicking in the playback button opens a screen with a list of recorded robots.
*
* After selecting the robots, press one of these buttons:
* <ul>
* <li> Play selected: plays the robots in the same order you selected (the order is recorded as you select).
* <li> Play all: plays all robots in the list, in the order they appear.
* <li> Play random: randomizes the selected robots order and reproduce that. A new window is opened asking you
* to enter the number of times that exact sequence will be run.
* <li> Dump contents: dumps the contents of the selected robots, so you can see the events that are played back.
* <li> Delete selected: delete the selected robots.
* </ul>
* Robots are saved in a file with ".robot" extension at the application's path. Since the robot saves absolute
* pen events at a specific time, they are not portable among different device resolutions and/or devices with
* different processors.
*
* You can start a specific robot passing its name as commandline to the application.
* For example, in:
* <ul>
* <li> Android: adb shell am start -a android.intent.action.MAIN -n totalcross.app.uigadgets/.UIGadgets -e cmdline test1.robot
* <li> Windows 32: UIGadgets.exe /cmdline test1.robot
* <li> J2SE: /cmdline flick.robot tc.samples.ui.gadgets.UIGadgets
* </ul>
*
* Note that no other commandline parameters must be passed to the application; the UIRobot expects that the only parameter
* will be the robot's name.
*
* In JavaSE (desktop), you can skip the robot start by launching the application and pressing the SHIFT key while the "Starting ..."
* MessageBox is displayed. This is useful if you want to record the robot again whithout having to remove it from the commandline.
* The shift key also aborts the robot during its execution.
*
* When the robot finishes, it takes a screenshot of the application. When it plays back, it compares the
* screen with the saved one and sends one of these two UIRobotEvent to the MainWindow: ROBOT_SUCCEED (if comparison succeeds) or
* ROBOT_FAILED (if comparison fails).
*
* You can create a log file with the results by putting this in the onEvent of your MainWindow class:
* <pre>
* if (event.type == UIRobotEvent.ROBOT_FAILED || event.type == UIRobotEvent.ROBOT_SUCCEED)
* {
* try
* {
* File f = new File(Settings.appPath+"/robot.log",File.CREATE_EMPTY);
* String s = event.type == UIRobotEvent.ROBOT_FAILED ? "FAILED" : "SUCCEED";
* f.writeBytes("Robot "+UIRobot.robotFileName+" "+s+". Running time: "+UIRobot.totalTime);
* f.close();
* }
* catch (Exception e) {e.printStackTrace();}
* }
* </pre>
*
* @see totalcross.sys.SpecialKeys
* @see UIRobotEvent
*/
public class UIRobot
{
private static final String TITLE = " User Interface Robot ";
public static final int IDLE = 0;
public static final int RECORDING = 1;
public static final int PLAYBACK = 2;
public static int status;
private static MainWindow mw = MainWindow.getMainWindow();
private int lastTS;
private File flog;
private DataStreamLE fds;
private int counter;
private boolean autolaunch;
/** Set to true to abort the run of the current UIRobot. Useful if you set a breakpoint
* at a code and want to abort the run.
*
* You can also abort the UIRobot by pressing the shift key during execution and at the starting message box.
*/
public static boolean abort;
/** The filename of the running robot. */
public static String robotFileName;
/** The amount of time since the robot started. */
public static int totalTime;
private static String[] recordedRobots;
/** Constructs a new UIRobot and opens the user interface.
*/
public UIRobot() throws Exception
{
autolaunch = false;
if (status != IDLE) // another robot already's running?
throw new Exception("Already running");
// get from user if he wants to record or playback
switch (showMessage("Please select the action:",new String[]{"Record","Playback","Cancel"},0,false))
{
case 0: record(); break;
case 1: playback(); break;
}
}
/** Constructs a new UIRobot and starts the playback of the given file. */
public UIRobot(String robotFileName) throws Exception
{
autolaunch = true;
recordedRobots = new String[]{robotFileName};
play(new int[]{0}, 1, false, 1);
}
private void record() throws Exception
{
InputBox ib = new InputBox(TITLE,"Please type the robot name:","",new String[]{"Start record","Cancel"});
ib.transitionEffect = Container.TRANSITION_NONE;
ib.setBackForeColors(Color.ORANGE, 1);
ib.buttonKeys = new int[]{SpecialKeys.ENTER,SpecialKeys.ESCAPE};
ib.popup();
if (ib.getPressedButtonIndex() == 1)
throw new Exception("Cancelled");
String name = ib.getValue() + ".robot";
flog = new File(robotFileName = Settings.appPath+"/"+name,File.CREATE_EMPTY);
fds = new DataStreamLE(flog);
lastTS = Vm.getTimeStamp();
status = RECORDING;
}
private void playback() throws Exception
{
if (recordedRobots == null)
fillListOfRecordedRobots();
if (recordedRobots == null)
{
showMessage("No robots found.",null,1500,false);
throw new Exception("No robots");
}
MultiListBox lb = new MultiListBox(recordedRobots);
lb.setOrderIsImportant(true);
ControlBox cb = new ControlBox(TITLE,"Select the robots in the\nsequence you want to run.",lb,Control.FILL,Control.FIT,
new String[]{"Play selected","Play all","Play random","Dump contents","Delete selected","Cancel"},2);
cb.transitionEffect = Container.TRANSITION_NONE;
cb.setBackForeColors(Color.ORANGE, 1);
cb.popup();
IntVector order = lb.getSelectedIndexes();
int n = order.size();
int sel = cb.getPressedButtonIndex();
switch (sel)
{
case 0: // play selected
play(order.items, n, false,1);
break;
case 1: // play all
play(null, recordedRobots.length, false,1);
break;
case 2: // play random
{
// get the number of repetitions
InputBox ib = new InputBox(TITLE,"Type the number of runs","1");
ib.getEdit().setValidChars(Edit.numbersSet);
ib.transitionEffect = Container.TRANSITION_NONE;
ib.setBackForeColors(Color.ORANGE, 1);
ib.popup();
if (ib.getPressedButtonIndex() == 1) // cancelled?
throw new Exception("Cancelled");
String countStr = ib.getValue();
int repeat;
try
{
repeat = Convert.toInt(countStr);
}
catch (InvalidNumberException ine)
{
showMessage("Invalid number, operation cancelled.",null,1500,true);
throw new Exception("Cancelled");
}
// fills an array with all indexes and them swap them randomly
int[] s = order.items;
Random r = new Random();
for (int i = n*3; --i >= 0;)
{
int idx1 = r.between(0,n-1);
int idx2 = r.between(0,n-1);
if (idx1 != idx2)
{
int temp = s[idx1];
s[idx1] = s[idx2];
s[idx2] = temp;
}
}
play(s, n, false, repeat);
break;
}
case 3: // dump
play(order.items, n, true, 1);
break;
case 4: // delete
if (showMessage("Do you want to delete the selected robots?", new String[]{"No","Yes"},0,true) == 1)
{
for (int i = 0; i < n; i++)
try
{
String item = recordedRobots[order.items[i]];
String fileName = item.substring(0,item.indexOf(' '));
new File(Settings.appPath+"/"+fileName,1).delete();
}
catch (Exception ee)
{
ee.printStackTrace();
}
fillListOfRecordedRobots();
}
break;
}
}
private Vector threadPool = new Vector(10);
private Lock tpLock;
private PostThread popThread()
{
try
{
synchronized (tpLock)
{
return (PostThread)threadPool.pop();
}
}
catch (ElementNotFoundException enfe)
{
PostThread t = new PostThread();
t.start();
return t;
}
}
private void pushThread(PostThread t)
{
synchronized (tpLock)
{
threadPool.push(t);
}
}
private class PostThread extends Thread
{
boolean running;
int type, key,x,y,mods;
Lock l;
public PostThread()
{
l = new Lock();
}
public void set(int type, int key, int x, int y, int mods)
{
this.key = key;
this.x = x;
this.y = y;
this.mods = mods;
synchronized (l)
{
this.type = type;
}
}
public void kill()
{
running = false;
}
public void run()
{
running = true;
while (running)
{
int type;
synchronized (l)
{
type = this.type;
}
if (type == 0)
Vm.sleep(1);
else
{
mw._postEvent(type, key, x, y, mods, 0);
this.type = 0;
pushThread(this);
}
}
}
}
private void play(final int[] items, final int n, final boolean dump, final int repeat)
{
if (tpLock == null)
tpLock = new Lock();
new Thread()
{
public void run()
{
String fileName = "";
try
{
ListBox lb = null;
ControlBox cb = null;
if (!dump)
status = PLAYBACK;
else
{
lb = new ListBox();
lb.enableHorizontalScroll();
cb = new ControlBox(TITLE,"Robot dump",lb,Control.FILL,Control.FIT, new String[]{"Ok"});
cb.transitionEffect = Container.TRANSITION_NONE;
cb.setBackForeColors(Color.ORANGE, 1);
}
abort = false;
for (int r = 1; !abort && r <= repeat && (dump || status == PLAYBACK); r++)
for (int i = 0; i < n && (dump || status == PLAYBACK); i++)
{
abort = false;
String item = recordedRobots[items == null ? i : items[i]];
fileName = item.substring(0,item.indexOf(' '));
File f = new File(fileName.indexOf('/') <= 0 ? Settings.appPath+"/"+fileName : fileName,File.READ_WRITE);
robotFileName = f.getPath();
DataStreamLE ds = new DataStreamLE(f);
String st = "Starting "+fileName;
if (repeat > 1)
st += " (run "+r+" of "+repeat+")";
Vm.debug(st);
if (!dump)
{
showMessage(st,null,1500,false);
if (Vm.isKeyDown(SpecialKeys.SHIFT))
abort = true;
}
else
lb.add(st);
totalTime = 0;
for (int j = 0; !abort && (dump || status == PLAYBACK); j++)
{
int type = ds.readInt();
int key = ds.readInt();
int x = ds.readInt();
int y = ds.readInt();
int mods = ds.readInt();
int delay = ds.readInt();
totalTime += delay;
if (!dump && delay > 0)
Vm.sleep(delay);
if (Vm.isKeyDown(SpecialKeys.SHIFT))
{
abort = true;
break;
}
if (dump || Settings.onJavaSE)
{
String s = dumpEvent(j,type,key,x,y,mods,delay);
if (!dump && Settings.onJavaSE)
Vm.debug(s);
if (lb != null) lb.add(s);
}
if (type == UIRobotEvent.ROBOT_EOF)
break;
if (!dump)
{
PostThread pt = popThread();
pt.set(type,key,x,y,mods);
}
}
f.close();
if (!dump && !abort)
{
Vm.sleep(1000);
Window.repaintActiveWindows();
try
{
boolean ok = compareScreenShots(fileName);
showMessage("Finished "+fileName+".\nScreenshot comparison "+(ok?"succeed":"FAILED"),null,ok ? 1500 : 2500, !ok);
}
catch (FileNotFoundException fnfe)
{
showMessage("Finished "+fileName+".\nScreenshot not found",null,1500,false);
Vm.debug("One of the image files was not found during robot test comparison.");
}
}
else
if (dump)
{
lb.add("====================");
}
}
if (dump)
{
cb.popup();
}
else
{
// kill all tasks in the thread pool
for (int i = threadPool.size(); --i >= 0;)
((PostThread)threadPool.items[i]).kill();
threadPool.removeAllElements();
if (!abort)
Vm.sleep(500); // give a time so all can get killed
else
if (Settings.onJavaSE)
Vm.debug("UIRobot ABORTED");
status = IDLE;
abort = false;
}
}
catch (Exception e)
{
e.printStackTrace();
status = IDLE;
robotFailed(fileName,"Exception thrown: "+e);
}
if (autolaunch)
recordedRobots = null;
}
}.start();
}
private String formatTime(int t)
{
int ms = t % 1000; t /= 1000;
int s = t % 60; t /= 60;
int m = t % 60; t /= 60;
int h = t % 24;
StringBuffer sb = new StringBuffer(20);
if (h > 0) sb.append(h).append('h');
if (h > 0 || m > 0) sb.append(m).append('m');
if (h > 0 || m > 0 || s > 0) sb.append(s).append('s');
if (ms < 100) sb.append('0');
if (ms < 10 ) sb.append('0');
sb.append(ms).append("ms");
return sb.toString();
}
private void robotFailed(String fileName, String reason)
{
Vm.debug("robot "+fileName+" failed");
UIRobotEvent ev = new UIRobotEvent(UIRobotEvent.ROBOT_FAILED, fileName, reason);
MainWindow.getMainWindow().postEvent(ev);
if (!ev.consumed && listeners != null)
for (int i = listeners.size(); --i >= 0 && !ev.consumed;)
((UIRobotListener)listeners.items[i]).robotFailed(ev);
}
private void robotSucceed(String fileName)
{
Vm.debug("robot "+fileName+" succeed");
UIRobotEvent ev = new UIRobotEvent(UIRobotEvent.ROBOT_SUCCEED, fileName, null);
MainWindow.getMainWindow().postEvent(ev);
if (!ev.consumed && listeners != null)
for (int i = listeners.size(); --i >= 0 && !ev.consumed;)
((UIRobotListener)listeners.items[i]).robotSucceed(ev);
}
private void saveScreenShot(String fileName, boolean recording) throws IOException, ImageException
{
fileName = getScreenShotName(fileName, recording);
File f = new File(fileName,File.CREATE_EMPTY);
Image img = MainWindow.getScreenShot();
img.createPng(f);
f.close();
}
private String getScreenShotName(String fileName, boolean recording)
{
return fileName.substring(0,fileName.length()-6) + (recording ? "_rec":"_play") + ".png";
}
private static byte []cmpbuf1, cmpbuf2;
private boolean compareScreenShots(String fileName) throws FileNotFoundException, IOException, ImageException
{
saveScreenShot(fileName, false);
// compare the recorded and playback images
if (cmpbuf1 == null)
{
cmpbuf1 = new byte[2048];
cmpbuf2 = new byte[2048];
}
String rec = getScreenShotName(fileName, true);
String ply = getScreenShotName(fileName, false);
File frec = new File(rec, File.READ_WRITE);
File fply = new File(ply, File.READ_WRITE);
byte[] cmp1 = cmpbuf1;
byte[] cmp2 = cmpbuf2;
int sr = frec.getSize();
int sp = fply.getSize();
boolean same = sr == sp;
while (same)
{
int r1 = frec.readBytes(cmp1,0,cmp1.length);
int r2 = fply.readBytes(cmp2,0,cmp2.length);
if (r1 <= 0)
break;
if (r1 != r2)
same = false;
else
while (--r1 >= 0)
if (cmp1[r1] != cmp2[r1])
same = false;
}
frec.close();
fply.close();
if (same)
robotSucceed(fileName);
else
robotFailed(fileName,"Screenshots don't match");
return same;
}
private String dumpEvent(int j, int type, int key, int x, int y, int mods, int delay)
{
switch (type)
{
case UIRobotEvent.ROBOT_EOF: return "ROBOT FINISHED"+" @ "+delay+"ms - "+formatTime(totalTime);
case PenEvent.PEN_DOWN: return "PEN_DOWN "+x+","+y+" @ "+delay+"ms - "+formatTime(totalTime);
case PenEvent.PEN_UP: return "PEN_UP "+x+","+y+" @ "+delay+"ms - "+formatTime(totalTime);
case PenEvent.PEN_DRAG: return "PEN_DRAG "+x+","+y+" @ "+delay+"ms - "+formatTime(totalTime);
case KeyEvent.KEY_PRESS: return "KEY_PRESS "+(key < 10 ? " " : key < 100 ? " " : "")+key+" '"+(char)key+"'"+(mods == 0 ? " @ " : " - "+mods+" @ ")+delay+"ms - "+formatTime(totalTime);
case KeyEvent.SPECIAL_KEY_PRESS: return "SPECIAL_KEY_PRESS "+key+(mods == 0 ? " @ " : " ("+mods+") @ ")+delay+"ms - "+formatTime(totalTime);
}
return "";
}
private int showMessage(String msg, String[] btns, int delay, boolean error)
{
MessageBox mb = delay > 0 ? new MessageBox(TITLE,msg,null) : btns == null ? new MessageBox(TITLE, msg) : new MessageBox(TITLE,msg,btns);
mb.transitionEffect = Container.TRANSITION_NONE;
mb.setBackForeColors(error ? Color.interpolate(Color.RED,Color.ORANGE) : Color.ORANGE, 1);
if (delay == 0)
mb.popup();
else
{
mb.popupNonBlocking();
Vm.sleep(delay);
mb.unpop();
}
return mb.getPressedButtonIndex();
}
/* Called from the Window class to record the event posted */
public void onEvent(int type, int key, int x, int y, int modifiers)
{
switch (type)
{
case PenEvent.PEN_DOWN:
case PenEvent.PEN_UP:
case PenEvent.PEN_DRAG:
case KeyEvent.KEY_PRESS:
case KeyEvent.SPECIAL_KEY_PRESS:
case UIRobotEvent.ROBOT_EOF:
// handle these events
break;
default:
// skip all others
return;
}
try
{
if (flog == null)
return;
int timestamp = Vm.getTimeStamp();
int elapsed = timestamp - lastTS;
lastTS = timestamp;
// int type, int key, int x, int y, int modifiers, int timeStamp
fds.writeInt(type);
fds.writeInt(key);
fds.writeInt(x);
fds.writeInt(y);
fds.writeInt(modifiers);
fds.writeInt(elapsed);
if (Settings.onJavaSE)
Vm.debug((counter++)+" "+type+" "+key+" "+x+" "+y+" "+modifiers+" "+elapsed);
}
catch (Exception ee)
{
MessageBox.showException(ee,true);
}
}
public void stop() throws Exception
{
if (status == RECORDING)
{
onEvent(UIRobotEvent.ROBOT_EOF,0,0,0,0);
flog.close();
flog = null;
saveScreenShot(robotFileName, true);
fillListOfRecordedRobots();
showMessage("Finished recording",null,2000,false);
}
status = IDLE;
}
private static class StrTime implements Comparable
{
String s;
long l;
StrTime(String s, Time t)
{
this.s = s;
l = t.getTimeLong();
}
public int compareTo(Object other) throws ClassCastException
{
StrTime st = (StrTime)other;
long res = l - st.l;
return res > 0 ? 1 : res < 0 ? -1 : 0;
}
}
private void fillListOfRecordedRobots() throws Exception
{
String[] list = new File(Settings.appPath).listFiles();
recordedRobots = null;
if (list != null && list.length > 0)
{
Vector v = new Vector(10);
for (int i =0; i < list.length; i++)
if (list[i].endsWith(".robot"))
{
File f = new File(Settings.appPath+"/"+list[i],File.READ_WRITE);
Time t = f.getTime(File.TIME_MODIFIED);
f.close();
v.addElement(new StrTime(list[i]+" ("+new Date(t)+" "+t+")", t));
}
int n = v.size();
if (n > 0)
{
v.qsort();
recordedRobots = new String[n];
while (--n >= 0)
recordedRobots[n] = ((StrTime)v.items[n]).s;
}
}
}
Vector listeners;
/** Adds a listener for UIRobot events.
* @see totalcross.unit.UIRobotListener
*/
public void addUIRobotListener(UIRobotListener listener)
{
if (listeners == null) listeners = new Vector(2);
listeners.addElement(listener);
}
/** Removes a listener for UIRobot events.
* @see totalcross.unit.UIRobotListener
*/
public void removeUIRobotListener(UIRobotListener listener)
{
listeners.removeElement(listener);
}
}