package com.simplbug.sikulimonkey; import java.awt.AWTException; import java.awt.Rectangle; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.regex.Matcher; import javax.imageio.ImageIO; import org.sikuli.script.Debug; import org.sikuli.script.FindFailed; import org.sikuli.script.IScreen; import org.sikuli.script.Location; import org.sikuli.script.Match; import org.sikuli.script.Pattern; import org.sikuli.script.Region; import org.sikuli.script.ScreenImage; import org.sikuli.script.Settings; public class AndroidRegion extends Match { private static double AUTO_WAIT_TIMEOUT = 20; protected AndroidRobot _robot; protected AndroidRegion() throws AWTException { super(0, 0, 0, 0, -1, null); _autoWaitTimeout = AUTO_WAIT_TIMEOUT; } public AndroidRegion(Match match, IScreen screen) throws AWTException { super(match.x, match.y, match.w, match.h, match.getScore(), screen); _robot = (AndroidRobot) screen.getRobot(); _autoWaitTimeout = AUTO_WAIT_TIMEOUT; } public AndroidRegion(Rectangle rect, IScreen screen) throws AWTException { super(rect.x, rect.y, rect.width, rect.height, -1, screen); _robot = (AndroidRobot) screen.getRobot(); _autoWaitTimeout = AUTO_WAIT_TIMEOUT; } public ScreenImage capture(String label) { return capture(label, false); } public ScreenImage capture(String label, boolean deleteOnExit) { ScreenImage img = _robot.captureScreen(getRect()); try { File tmp = File.createTempFile("sikuli-scr-" + label + "-", ".png"); if (deleteOnExit) tmp.deleteOnExit(); Debug.history("[" + label + "] Region capture -> " + tmp.getPath()); ImageIO.write(img.getImage(), "png", tmp); } catch (IOException e) { throw new RuntimeException(e); } return img; } @Override public <PSC> Match exists(PSC target, double timeout) { return newMatch(super.exists(getAlternativePS(target), timeout)); } public <PSRML> int tap(PSRML target) throws FindFailed { Location loc = getLocationFromPSRML(target); _robot.tap(loc.x, loc.y); return 1; } public <PSRML> int longPress(PSRML target) throws FindFailed { Location loc = getLocationFromPSRML(target); _robot.longPress(loc.x, loc.y); return 1; } public <PSRML> int pan(PSRML arg0, PSRML arg1) throws FindFailed { return super.dragDrop(arg0, arg1); } public <PSRML> int type(PSRML target, String text) throws FindFailed { if (target != null) { tap(target); try { Thread.sleep(2 * 1000); } catch (InterruptedException e) { } } _robot.type(text); try { Thread.sleep(2 * 1000); } catch (InterruptedException e) { } return 1; } @Override public <PSC> Match find(PSC target) throws FindFailed { return newMatch(super.find(getAlternativePS(target))); } @Override public <PSC> Iterator<Match> findAll(PSC target) throws FindFailed { Iterator<Match> all = super.findAll(getAlternativePS(target)); ArrayList<Match> wrappers = new ArrayList<Match>(); while (all.hasNext()) wrappers.add(newMatch(all.next())); return wrappers.iterator(); } @Override public <PSC> Iterator<Match> findAllNow(PSC target) throws FindFailed { Iterator<Match> all = super.findAllNow(getAlternativePS(target)); ArrayList<Match> wrappers = new ArrayList<Match>(); while (all.hasNext()) wrappers.add(newMatch(all.next())); return wrappers.iterator(); } @Override public <PSC> Match wait(PSC target, double timeout) throws FindFailed { return newMatch(super.wait(getAlternativePS(target), timeout)); } @Override public <PSC> boolean waitVanish(PSC target, double timeout) { return super.waitVanish(getAlternativePS(target), timeout); } @Override public Region offset(Location loc) { Rectangle rect = new Rectangle(x+loc.x, y+loc.y, w, h); return newRegion(rect); } @Override public Region nearby(int range) { Rectangle bounds = getScreen().getBounds(); Rectangle rect = new Rectangle(x - range, y - range, w + range * 2, h + range * 2); rect = rect.intersection(bounds); return newRegion(rect); } @Override public Region right(int range) { Rectangle bounds = getScreen().getBounds(); Rectangle rect = new Rectangle(x + w, y, range, h); rect = rect.intersection(bounds); return newRegion(rect); } @Override public Region left(int range) { Rectangle bounds = getScreen().getBounds(); Region r = newRegion(getRect()); r.x = x-range < bounds.x? bounds.x: x-range; r.y = y; r.w = x - r.x; r.h = h; return r; } @Override public Region above(int range) { Rectangle bounds = getScreen().getBounds(); Region r = newRegion(getRect()); r.x = x; r.y = y-range < bounds.y? bounds.y : y-range; r.w = w; r.h = y-r.y; return r; } @Override public Region below(int range) { Rectangle bounds = getScreen().getBounds(); Rectangle rect = new Rectangle(x, y + h, w, range); rect = rect.intersection(bounds); return newRegion(rect); } protected Region newRegion(Rectangle rect) { try { return new AndroidRegion(rect, getScreen()); } catch (AWTException e) { throw new RuntimeException(e); } } protected Match newMatch(Match match) { if (match == null) return null; try { return new AndroidRegion(match, getScreen()); // TODO: target offset should be kept, but Match.setTargetOffset() is only accessible in the same package. } catch (AWTException e) { throw new RuntimeException(e); } } @Override public <PSRML> Location getLocationFromPSRML(PSRML target) throws FindFailed { Location loc = super.getLocationFromPSRML(getAlternativePS(target)); if (target instanceof Pattern) { Location offset = ((Pattern)target).getTargetOffset(); loc.translate(offset.x, offset.y); } return loc; } @SuppressWarnings("unchecked") private <PS> PS getAlternativePS(PS target) { if (target instanceof String) { target = (PS)getAlternativeFilename((String)target); } else if (target instanceof Pattern) { Pattern pattern = (Pattern)target; String filename = pattern.getFilename(); String altFilename = getAlternativeFilename(filename); if (!altFilename.equals(filename)) { float similarity = extractPatternSimilarity(pattern); String debug = pattern.toString(); pattern = new Pattern(altFilename); pattern = pattern.similar(similarity); Debug.history("Alternative pattern; " + debug + " --> " + pattern.toString()); } target = (PS)pattern; } return target; } private float extractPatternSimilarity(Pattern pattern) { // tricky // Pattern("/path/to/image.png").similar(0.7) Matcher matcher = java.util.regex.Pattern.compile( "\\.similar\\((.+?)\\)").matcher(pattern.toString()); boolean found = matcher.find(); assert found : pattern.toString(); return Float.valueOf(matcher.group(1)); } private String getAlternativeFilename(String filename) { // not a filename with the extension, or no alternative if (!filename.contains(".")) return filename; assert Settings.BundlePath != null; File file = new File(filename); if (!file.isAbsolute()) file = new File(Settings.BundlePath, filename); if (!file.exists()) { Debug.history("The image file passed in (" + filename + ") doesn't exist, so it might be a string for OCR"); return filename; // might be a string for OCR } String ext = filename.substring(filename.lastIndexOf(".")); String base_in = filename.substring(0, filename.lastIndexOf(".")); String base = base_in.contains("__") ? base_in.substring(0, base_in.lastIndexOf("__")) : base_in; String device_id = _robot.getModel().replace(' ', '_').toLowerCase(); // try device-specific file in the same folder first String fname_dev = base + "__" + device_id + ext; if (fname_dev.equals(filename)) { Debug.history("The image file passed in (" + filename + ") is already for the device under test."); return filename; } File file_dev = new File(fname_dev); if (!file_dev.isAbsolute()) file_dev = new File(Settings.BundlePath, fname_dev); if (file_dev.exists()) { Debug.history("The image file specific to the device under test exists. (" + filename + " -> " + fname_dev + ")"); return fname_dev; } // try device-specific file in the sibling folder, named by the module name File sibling_dir = new File(file.getParentFile().getParentFile().getPath(), device_id); file_dev = new File(sibling_dir, new File(base).getName() + ext); if (file_dev.exists()) { Debug.history("The image file specific to the device under test exists in the sibling folder. (" + filename + " -> " + file_dev.getPath() + ")"); return file_dev.getPath(); } // try generic one, if the filename passed in is an device-specific one. if (base != base_in) { String fname_comm = base + ext; File file_comm = new File(fname_comm); if (!file_comm.isAbsolute()) file_comm = new File(Settings.BundlePath, fname_comm); if (file_comm.exists()) { Debug.history("The image file passed in (" + filename + ") is for another device. The common version (" + fname_comm + ") will be used."); return fname_comm; } } Debug.history("Although the string passed in (" + filename + ") seems like a file name, all possible variations (model: " + device_id + ") do not exist. The string will be used as is."); return filename; } }