diff options
author | Michael Wright <michaelwr@google.com> | 2011-06-13 09:15:08 -0700 |
---|---|---|
committer | Michael Wright <michaelwr@google.com> | 2011-06-20 14:24:05 -0700 |
commit | 7af6646391705a276b814bf8b45f8874554831fb (patch) | |
tree | 5d17901bc229bdb1c998c52027e4a7610fb43796 /chimpchat | |
parent | 5773adb742c8915f9f72b47ab8f00ed4f79357ae (diff) | |
download | sdk-7af6646391705a276b814bf8b45f8874554831fb.zip sdk-7af6646391705a276b814bf8b45f8874554831fb.tar.gz sdk-7af6646391705a276b814bf8b45f8874554831fb.tar.bz2 |
Extracted ChimpChat from MonkeyRunner
Extracted ChimpChat, the library for communication with Monkey from
MonkeyRunner
Change-Id: Ia9f966549d27abc9f494b2b001099d8130dea376
Diffstat (limited to 'chimpchat')
20 files changed, 2530 insertions, 3 deletions
diff --git a/chimpchat/src/Android.mk b/chimpchat/src/Android.mk index 301ec71..82a2052 100644 --- a/chimpchat/src/Android.mk +++ b/chimpchat/src/Android.mk @@ -18,6 +18,13 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAVA_LIBRARIES := \ + ddmlib \ + hierarchyviewerlib \ + guavalib \ + sdklib \ + swt + LOCAL_MODULE := chimpchat LOCAL_MODULE_TAGS := eng diff --git a/chimpchat/src/com/android/chimpchat/ChimpChat.java b/chimpchat/src/com/android/chimpchat/ChimpChat.java index 71684c9..1927535 100644 --- a/chimpchat/src/com/android/chimpchat/ChimpChat.java +++ b/chimpchat/src/com/android/chimpchat/ChimpChat.java @@ -17,12 +17,44 @@ package com.android.chimpchat; +import com.android.chimpchat.adb.AdbBackend; +import com.android.chimpchat.core.IChimpBackend; + import java.util.Map; +/** + * ChimpChat is a host-side library that provides an API for communication with + * an instance of Monkey on a device. This class provides an entry point to + * setting up communication with a device. Currently it only supports communciation + * over ADB, however. + */ public class ChimpChat { - private final Map<String, String> options; + private final IChimpBackend mBackend; + + private ChimpChat(IChimpBackend backend) { + this.mBackend = backend; + } + + /** + * Generates a new instance of ChimpChat based on the options passed. + * @param options a map of settings for the new ChimpChat instance + * @return a new instance of ChimpChat or null if there was an issue setting up the backend + */ + public static ChimpChat getInstance(Map<String, String> options) { + IChimpBackend backend = ChimpChat.createBackendByName(options.get("backend")); + if (backend == null) { + return null; + } + ChimpChat chimpchat = new ChimpChat(backend); + return chimpchat; + } + - public ChimpChat(Map<String, String> options) { - this.options = options; + public static IChimpBackend createBackendByName(String backendName) { + if ("adb".equals(backendName)) { + return new AdbBackend(); + } else { + return null; + } } } diff --git a/chimpchat/src/com/android/chimpchat/ChimpManager.java b/chimpchat/src/com/android/chimpchat/ChimpManager.java new file mode 100644 index 0000000..a858e6a --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/ChimpManager.java @@ -0,0 +1,350 @@ + +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat; + +import com.google.common.collect.Lists; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.util.Collection; +import java.util.Collections; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides a nicer interface to interacting with the low-level network access protocol for talking + * to the monkey. + * + * This class is thread-safe and can handle being called from multiple threads. + */ +public class ChimpManager { + private static Logger LOG = Logger.getLogger(ChimpManager.class.getName()); + + private Socket monkeySocket; + private BufferedWriter monkeyWriter; + private BufferedReader monkeyReader; + + /** + * Create a new ChimpMananger to talk to the specified device. + * + * @param monkeySocket the already connected socket on which to send protocol messages. + * @throws IOException if there is an issue setting up the sockets + */ + public ChimpManager(Socket monkeySocket) throws IOException { + this.monkeySocket = monkeySocket; + monkeyWriter = + new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); + monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); + } + + /** + * Send a touch down event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchDown(int x, int y) throws IOException { + return sendMonkeyEvent("touch down " + x + " " + y); + } + + /** + * Send a touch down event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchUp(int x, int y) throws IOException { + return sendMonkeyEvent("touch up " + x + " " + y); + } + + /** + * Send a touch move event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchMove(int x, int y) throws IOException { + return sendMonkeyEvent("touch move " + x + " " + y); + } + + /** + * Send a touch (down and then up) event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touch(int x, int y) throws IOException { + return sendMonkeyEvent("tap " + x + " " + y); + } + + /** + * Press a physical button on the device. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean press(String name) throws IOException { + return sendMonkeyEvent("press " + name); + } + + /** + * Send a Key Down event for the specified button. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean keyDown(String name) throws IOException { + return sendMonkeyEvent("key down " + name); + } + + /** + * Send a Key Up event for the specified button. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean keyUp(String name) throws IOException { + return sendMonkeyEvent("key up " + name); + } + + /** + * Press a physical button on the device. + * + * @param button the button to press + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean press(PhysicalButton button) throws IOException { + return press(button.getKeyName()); + } + + /** + * This function allows the communication bridge between the host and the device + * to be invisible to the script for internal needs. + * It splits a command into monkey events and waits for responses for each over an adb tcp socket. + * Returns on an error, else continues and sets up last response. + * + * @param command the monkey command to send to the device + * @return the (unparsed) response returned from the monkey. + */ + private String sendMonkeyEventAndGetResponse(String command) throws IOException { + command = command.trim(); + LOG.info("Monkey Command: " + command + "."); + + // send a single command and get the response + monkeyWriter.write(command + "\n"); + monkeyWriter.flush(); + return monkeyReader.readLine(); + } + + /** + * Parse a monkey response string to see if the command succeeded or not. + * + * @param monkeyResponse the response + * @return true if response code indicated success. + */ + private boolean parseResponseForSuccess(String monkeyResponse) { + if (monkeyResponse == null) { + return false; + } + // return on ok + if(monkeyResponse.startsWith("OK")) { + return true; + } + + return false; + } + + /** + * Parse a monkey response string to get the extra data returned. + * + * @param monkeyResponse the response + * @return any extra data that was returned, or empty string if there was nothing. + */ + private String parseResponseForExtra(String monkeyResponse) { + int offset = monkeyResponse.indexOf(':'); + if (offset < 0) { + return ""; + } + return monkeyResponse.substring(offset + 1); + } + + /** + * This function allows the communication bridge between the host and the device + * to be invisible to the script for internal needs. + * It splits a command into monkey events and waits for responses for each over an + * adb tcp socket. + * + * @param command the monkey command to send to the device + * @return true on success. + */ + private boolean sendMonkeyEvent(String command) throws IOException { + synchronized (this) { + String monkeyResponse = sendMonkeyEventAndGetResponse(command); + return parseResponseForSuccess(monkeyResponse); + } + } + + /** + * Close all open resources related to this device. + */ + public void close() { + try { + monkeySocket.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeySocket", e); + } + try { + monkeyReader.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeyReader", e); + } + try { + monkeyWriter.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e); + } + } + + /** + * Function to get a static variable from the device. + * + * @param name name of static variable to get + * @return the value of the variable, or null if there was an error + */ + public String getVariable(String name) throws IOException { + synchronized (this) { + String response = sendMonkeyEventAndGetResponse("getvar " + name); + if (!parseResponseForSuccess(response)) { + return null; + } + return parseResponseForExtra(response); + } + } + + /** + * Function to get the list of static variables from the device. + */ + public Collection<String> listVariable() throws IOException { + synchronized (this) { + String response = sendMonkeyEventAndGetResponse("listvar"); + if (!parseResponseForSuccess(response)) { + Collections.emptyList(); + } + String extras = parseResponseForExtra(response); + return Lists.newArrayList(extras.split(" ")); + } + } + + /** + * Tells the monkey that we are done for this session. + * @throws IOException + */ + public void done() throws IOException { + // this command just drops the connection, so handle it here + synchronized (this) { + sendMonkeyEventAndGetResponse("done"); + } + } + + /** + * Tells the monkey that we are done forever. + * @throws IOException + */ + public void quit() throws IOException { + // this command drops the connection, so handle it here + synchronized (this) { + sendMonkeyEventAndGetResponse("quit"); + } + } + + /** + * Send a tap event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException + * @throws IOException on error communicating with the device + */ + public boolean tap(int x, int y) throws IOException { + return sendMonkeyEvent("tap " + x + " " + y); + } + + /** + * Type the following string to the monkey. + * + * @param text the string to type + * @return success + * @throws IOException + */ + public boolean type(String text) throws IOException { + // The network protocol can't handle embedded line breaks, so we have to handle it + // here instead + StringTokenizer tok = new StringTokenizer(text, "\n", true); + while (tok.hasMoreTokens()) { + String line = tok.nextToken(); + if ("\n".equals(line)) { + boolean success = press(PhysicalButton.ENTER); + if (!success) { + return false; + } + } else { + boolean success = sendMonkeyEvent("type " + line); + if (!success) { + return false; + } + } + } + return true; + } + + /** + * Type the character to the monkey. + * + * @param keyChar the character to type. + * @return success + * @throws IOException + */ + public boolean type(char keyChar) throws IOException { + return type(Character.toString(keyChar)); + } + + /** + * Wake the device up from sleep. + * @throws IOException + */ + public void wake() throws IOException { + sendMonkeyEvent("wake"); + } +} diff --git a/chimpchat/src/com/android/chimpchat/PhysicalButton.java b/chimpchat/src/com/android/chimpchat/PhysicalButton.java new file mode 100644 index 0000000..9363c08 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/PhysicalButton.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat; + +public enum PhysicalButton { + HOME("home"), + SEARCH("search"), + MENU("menu"), + BACK("back"), + DPAD_UP("DPAD_UP"), + DPAD_DOWN("DPAD_DOWN"), + DPAD_LEFT("DPAD_LEFT"), + DPAD_RIGHT("DPAD_RIGHT"), + DPAD_CENTER("DPAD_CENTER"), + ENTER("enter"); + + private String keyName; + + private PhysicalButton(String keyName) { + this.keyName = keyName; + } + + public String getKeyName() { + return keyName; + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbBackend.java b/chimpchat/src/com/android/chimpchat/adb/AdbBackend.java new file mode 100644 index 0000000..e2f9ca0 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/AdbBackend.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + +import com.google.common.collect.Lists; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.chimpchat.core.IChimpBackend; +import com.android.chimpchat.core.IChimpDevice; +import com.android.sdklib.SdkConstants; + +import java.io.File; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * Backend implementation that works over ADB to talk to the device. + */ +public class AdbBackend implements IChimpBackend { + private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName()); + // How long to wait each time we check for the device to be connected. + private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200; + private final List<IChimpDevice> devices = Lists.newArrayList(); + private final AndroidDebugBridge bridge; + + public AdbBackend() { + // [try to] ensure ADB is running + String adbLocation = findAdb(); + + AndroidDebugBridge.init(false /* debugger support */); + + bridge = AndroidDebugBridge.createBridge( + adbLocation, true /* forceNewBridge */); + } + + private String findAdb() { + File location = + new File(AdbBackend.class.getProtectionDomain().getCodeSource().getLocation().getPath()); + String mrParentLocation = new File(location.getParent()).getParent(); + + // in the new SDK, adb is in the platform-tools, but when run from the command line + // in the Android source tree, then adb is next to monkeyrunner. + if (mrParentLocation != null && mrParentLocation.length() != 0) { + // check if there's a platform-tools folder + File platformTools = new File(new File(mrParentLocation).getParent(), + SdkConstants.FD_PLATFORM_TOOLS); + if (platformTools.isDirectory()) { + return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB; + } + + return mrParentLocation + File.separator + SdkConstants.FD_OUTPUT + + File.separator + SdkConstants.FN_ADB; + } + + return SdkConstants.FN_ADB; + } + + /** + * Checks the attached devices looking for one whose device id matches the specified regex. + * + * @param deviceIdRegex the regular expression to match against + * @return the Device (if found), or null (if not found). + */ + private IDevice findAttachedDevice(String deviceIdRegex) { + Pattern pattern = Pattern.compile(deviceIdRegex); + for (IDevice device : bridge.getDevices()) { + String serialNumber = device.getSerialNumber(); + if (pattern.matcher(serialNumber).matches()) { + return device; + } + } + return null; + } + + @Override + public IChimpDevice waitForConnection() { + return waitForConnection(Integer.MAX_VALUE, ".*"); + } + + @Override + public IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex) { + do { + IDevice device = findAttachedDevice(deviceIdRegex); + // Only return the device when it is online + if (device != null && device.getState() == IDevice.DeviceState.ONLINE) { + IChimpDevice chimpDevice = new AdbChimpDevice(device); + devices.add(chimpDevice); + return chimpDevice; + } + + try { + Thread.sleep(CONNECTION_ITERATION_TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + timeoutMs -= CONNECTION_ITERATION_TIMEOUT_MS; + } while (timeoutMs > 0); + + // Timeout. Give up. + return null; + } + + @Override + public void shutdown() { + for (IChimpDevice device : devices) { + device.dispose(); + } + AndroidDebugBridge.terminate(); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java b/chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java new file mode 100644 index 0000000..5b70148 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/AdbChimpDevice.java @@ -0,0 +1,557 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.InstallException; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.chimpchat.ChimpManager; +import com.android.chimpchat.adb.LinearInterpolator.Point; +import com.android.chimpchat.core.IChimpImage; +import com.android.chimpchat.core.IChimpDevice; +import com.android.chimpchat.core.TouchPressType; +import com.android.chimpchat.hierarchyviewer.HierarchyViewer; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +public class AdbChimpDevice implements IChimpDevice { + private static final Logger LOG = Logger.getLogger(AdbChimpDevice.class.getName()); + + private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0]; + private static final long MANAGER_CREATE_TIMEOUT_MS = 30 * 1000; // 30 seconds + private static final long MANAGER_CREATE_WAIT_TIME_MS = 1000; // wait 1 second + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + private final IDevice device; + private ChimpManager manager; + + public AdbChimpDevice(IDevice device) { + this.device = device; + this.manager = createManager("127.0.0.1", 12345); + + Preconditions.checkNotNull(this.manager); + } + + @Override + public ChimpManager getManager() { + return manager; + } + + @Override + public void dispose() { + try { + manager.quit(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error getting the manager to quit", e); + } + manager = null; + } + + @Override + public HierarchyViewer getHierarchyViewer() { + return new HierarchyViewer(device); + } + + private void executeAsyncCommand(final String command, + final LoggingOutputReceiver logger) { + executor.submit(new Runnable() { + @Override + public void run() { + try { + device.executeShellCommand(command, logger); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (ShellCommandUnresponsiveException e) { + // This happens a lot + LOG.log(Level.INFO, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } + } + }); + } + + private ChimpManager createManager(String address, int port) { + try { + device.createForward(port, port); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e); + return null; + } + + String command = "monkey --port " + port; + executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE)); + + // Sleep for a second to give the command time to execute. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Unable to sleep", e); + } + + InetAddress addr; + try { + addr = InetAddress.getByName(address); + } catch (UnknownHostException e) { + LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e); + return null; + } + + // We have a tough problem to solve here. "monkey" on the device gives us no indication + // when it has started up and is ready to serve traffic. If you try too soon, commands + // will fail. To remedy this, we will keep trying until a single command (in this case, + // wake) succeeds. + boolean success = false; + ChimpManager mm = null; + long start = System.currentTimeMillis(); + + while (!success) { + long now = System.currentTimeMillis(); + long diff = now - start; + if (diff > MANAGER_CREATE_TIMEOUT_MS) { + LOG.severe("Timeout while trying to create chimp mananger"); + return null; + } + + try { + Thread.sleep(MANAGER_CREATE_WAIT_TIME_MS); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Unable to sleep", e); + } + + Socket monkeySocket; + try { + monkeySocket = new Socket(addr, port); + } catch (IOException e) { + LOG.log(Level.FINE, "Unable to connect socket", e); + success = false; + continue; + } + + try { + mm = new ChimpManager(monkeySocket); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to open writer and reader to socket"); + continue; + } + + try { + mm.wake(); + } catch (IOException e) { + LOG.log(Level.FINE, "Unable to wake up device", e); + success = false; + continue; + } + success = true; + } + + return mm; + } + + @Override + public IChimpImage takeSnapshot() { + try { + return new AdbChimpImage(device.getScreenshot()); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } + } + + @Override + public String getSystemProperty(String key) { + return device.getProperty(key); + } + + @Override + public String getProperty(String key) { + try { + return manager.getVariable(key); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to get variable: " + key, e); + return null; + } + } + + @Override + public void wake() { + try { + manager.wake(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e); + } + } + + private String shell(String... args) { + StringBuilder cmd = new StringBuilder(); + for (String arg : args) { + cmd.append(arg).append(" "); + } + return shell(cmd.toString()); + } + + @Override + public String shell(String cmd) { + CommandOutputCapture capture = new CommandOutputCapture(); + try { + device.executeShellCommand(cmd, capture); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (ShellCommandUnresponsiveException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } + return capture.toString(); + } + + @Override + public boolean installPackage(String path) { + try { + String result = device.installPackage(path, true); + if (result != null) { + LOG.log(Level.SEVERE, "Got error installing package: "+ result); + return false; + } + return true; + } catch (InstallException e) { + LOG.log(Level.SEVERE, "Error installing package: " + path, e); + return false; + } + } + + @Override + public boolean removePackage(String packageName) { + try { + String result = device.uninstallPackage(packageName); + if (result != null) { + LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " + + result); + return false; + } + return true; + } catch (InstallException e) { + LOG.log(Level.SEVERE, "Error installing package: " + packageName, e); + return false; + } + } + + @Override + public void press(String keyName, TouchPressType type) { + try { + switch (type) { + case DOWN_AND_UP: + manager.press(keyName); + break; + case DOWN: + manager.keyDown(keyName); + break; + case UP: + manager.keyUp(keyName); + break; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e); + } + } + + @Override + public void type(String string) { + try { + manager.type(string); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error Typing: " + string, e); + } + } + + @Override + public void touch(int x, int y, TouchPressType type) { + try { + switch (type) { + case DOWN: + manager.touchDown(x, y); + break; + case UP: + manager.touchUp(x, y); + break; + case DOWN_AND_UP: + manager.tap(x, y); + break; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e); + } + } + + @Override + public void reboot(String into) { + try { + device.reboot(into); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } + } + + @Override + public void startActivity(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, + extras, component, flags); + shell(Lists.asList("am", "start", + intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); + } + + @Override + public void broadcastIntent(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, + extras, component, flags); + shell(Lists.asList("am", "broadcast", + intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); + } + + private static boolean isNullOrEmpty(@Nullable String string) { + return string == null || string.length() == 0; + } + + private List<String> buildIntentArgString(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> parts = Lists.newArrayList(); + + // from adb docs: + //<INTENT> specifications include these flags: + // [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>] + // [-c <CATEGORY> [-c <CATEGORY>] ...] + // [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...] + // [--esn <EXTRA_KEY> ...] + // [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...] + // [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...] + // [-n <COMPONENT>] [-f <FLAGS>] + // [<URI>] + + if (!isNullOrEmpty(action)) { + parts.add("-a"); + parts.add(action); + } + + if (!isNullOrEmpty(data)) { + parts.add("-d"); + parts.add(data); + } + + if (!isNullOrEmpty(mimetype)) { + parts.add("-t"); + parts.add(mimetype); + } + + // Handle categories + for (String category : categories) { + parts.add("-c"); + parts.add(category); + } + + // Handle extras + for (Entry<String, Object> entry : extras.entrySet()) { + // Extras are either boolean, string, or int. See which we have + Object value = entry.getValue(); + String valueString; + String arg; + if (value instanceof Integer) { + valueString = Integer.toString((Integer) value); + arg = "--ei"; + } else if (value instanceof Boolean) { + valueString = Boolean.toString((Boolean) value); + arg = "--ez"; + } else { + // treat is as a string. + valueString = value.toString(); + arg = "--es"; + } + parts.add(arg); + parts.add(entry.getKey()); + parts.add(valueString); + } + + if (!isNullOrEmpty(component)) { + parts.add("-n"); + parts.add(component); + } + + if (flags != 0) { + parts.add("-f"); + parts.add(Integer.toString(flags)); + } + + if (!isNullOrEmpty(uri)) { + parts.add(uri); + } + + return parts; + } + + @Override + public Map<String, Object> instrument(String packageName, Map<String, Object> args) { + List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r", packageName); + String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY)); + return convertInstrumentResult(result); + } + + /** + * Convert the instrumentation result into it's Map representation. + * + * @param result the result string + * @return the new map + */ + @VisibleForTesting + /* package */ static Map<String, Object> convertInstrumentResult(String result) { + Map<String, Object> map = Maps.newHashMap(); + Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE); + Matcher matcher = pattern.matcher(result); + + int previousEnd = 0; + String previousWhich = null; + + while (matcher.find()) { + if ("RESULT".equals(previousWhich)) { + String resultLine = result.substring(previousEnd, matcher.start()).trim(); + // Look for the = in the value, and split there + int splitIndex = resultLine.indexOf("="); + String key = resultLine.substring(0, splitIndex); + String value = resultLine.substring(splitIndex + 1); + + map.put(key, value); + } + + previousEnd = matcher.end(); + previousWhich = matcher.group(1); + } + if ("RESULT".equals(previousWhich)) { + String resultLine = result.substring(previousEnd, matcher.start()).trim(); + // Look for the = in the value, and split there + int splitIndex = resultLine.indexOf("="); + String key = resultLine.substring(0, splitIndex); + String value = resultLine.substring(splitIndex + 1); + + map.put(key, value); + } + return map; + } + + @Override + public void drag(int startx, int starty, int endx, int endy, int steps, long ms) { + final long iterationTime = ms / steps; + + LinearInterpolator lerp = new LinearInterpolator(steps); + LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty); + LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy); + lerp.interpolate(start, end, new LinearInterpolator.Callback() { + @Override + public void step(Point point) { + try { + manager.touchMove(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag start event", e); + } + + try { + Thread.sleep(iterationTime); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + } + + @Override + public void start(Point point) { + try { + manager.touchDown(point.getX(), point.getY()); + manager.touchMove(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag start event", e); + } + + try { + Thread.sleep(iterationTime); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + } + + @Override + public void end(Point point) { + try { + manager.touchMove(point.getX(), point.getY()); + manager.touchUp(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag end event", e); + } + } + }); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java b/chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java new file mode 100644 index 0000000..2d41600 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/AdbChimpImage.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + +import com.android.ddmlib.RawImage; +import com.android.chimpchat.adb.image.ImageUtils; +import com.android.chimpchat.core.ChimpImageBase; + +import java.awt.image.BufferedImage; + +/** + * ADB implementation of the ChimpImage class. + */ +public class AdbChimpImage extends ChimpImageBase { + private final RawImage image; + + /** + * Create a new AdbMonkeyImage. + * + * @param image the image from adb. + */ + AdbChimpImage(RawImage image) { + this.image = image; + } + + @Override + public BufferedImage createBufferedImage() { + return ImageUtils.convertImage(image); + } + + public RawImage getRawImage() { + return image; + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java b/chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java new file mode 100644 index 0000000..eadd697 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/CommandOutputCapture.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + +import com.android.ddmlib.IShellOutputReceiver; + +/** + * Shell Output Receiver that captures shell output into a String for + * later retrieval. + */ +public class CommandOutputCapture implements IShellOutputReceiver { + private final StringBuilder builder = new StringBuilder(); + + public void flush() { } + + public boolean isCancelled() { + return false; + } + + public void addOutput(byte[] data, int offset, int length) { + String message = new String(data, offset, length); + builder.append(message); + } + + @Override + public String toString() { + return builder.toString(); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java b/chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java new file mode 100644 index 0000000..708007d --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/LinearInterpolator.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + + + +/** + * Linear Interpolation class. + */ +public class LinearInterpolator { + private final int steps; + + /** + * Use our own Point class so we don't pull in java.awt.* just for this simple class. + */ + public static class Point { + private final int x; + private final int y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return new StringBuilder(). + append("("). + append(x). + append(","). + append(y). + append(")").toString(); + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof Point) { + Point that = (Point) obj; + return this.x == that.x && this.y == that.y; + } + return false; + } + + @Override + public int hashCode() { + return 0x43125315 + x + y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + } + + /** + * Callback interface to recieve interpolated points. + */ + public interface Callback { + /** + * Called once to inform of the start point. + */ + void start(Point point); + /** + * Called once to inform of the end point. + */ + void end(Point point); + /** + * Called at every step in-between start and end. + */ + void step(Point point); + } + + /** + * Create a new linear Interpolator. + * + * @param steps How many steps should be in a single run. This counts the intervals + * in-between points, so the actual number of points generated will be steps + 1. + */ + public LinearInterpolator(int steps) { + this.steps = steps; + } + + // Copied from android.util.MathUtils since we couldn't link it in on the host. + private static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + /** + * Calculate the interpolated points. + * + * @param start The starting point + * @param end The ending point + * @param callback the callback to call with each calculated points. + */ + public void interpolate(Point start, Point end, Callback callback) { + int xDistance = Math.abs(end.getX() - start.getX()); + int yDistance = Math.abs(end.getY() - start.getY()); + float amount = (float) (1.0 / steps); + + + callback.start(start); + for (int i = 1; i < steps; i++) { + float newX = lerp(start.getX(), end.getX(), amount * i); + float newY = lerp(start.getY(), end.getY(), amount * i); + + callback.step(new Point(Math.round(newX), Math.round(newY))); + } + // Generate final point + callback.end(end); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java b/chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java new file mode 100644 index 0000000..e318a01 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/LoggingOutputReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb; + +import com.android.ddmlib.IShellOutputReceiver; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Shell Output Receiver that sends shell output to a Logger. + */ +public class LoggingOutputReceiver implements IShellOutputReceiver { + private final Logger log; + private final Level level; + + public LoggingOutputReceiver(Logger log, Level level) { + this.log = log; + this.level = level; + } + + public void addOutput(byte[] data, int offset, int length) { + String message = new String(data, offset, length); + for (String line : message.split("\n")) { + log.log(level, line); + } + } + + public void flush() { } + + public boolean isCancelled() { + return false; + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java b/chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java new file mode 100644 index 0000000..2b700ea --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb.image; + +import com.android.ddmlib.RawImage; +import com.android.chimpchat.adb.AdbBackend; +import com.android.chimpchat.adb.AdbChimpImage; +import com.android.chimpchat.core.IChimpBackend; +import com.android.chimpchat.core.IChimpImage; +import com.android.chimpchat.core.IChimpDevice; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * Utility program to capture raw and converted images from a device and write them to a file. + * This is used to generate the test data for ImageUtilsTest. + */ +public class CaptureRawAndConvertedImage { + public static class ChimpRunnerRawImage implements Serializable { + public int version; + public int bpp; + public int size; + public int width; + public int height; + public int red_offset; + public int red_length; + public int blue_offset; + public int blue_length; + public int green_offset; + public int green_length; + public int alpha_offset; + public int alpha_length; + + public byte[] data; + + public ChimpRunnerRawImage(RawImage rawImage) { + version = rawImage.version; + bpp = rawImage.bpp; + size = rawImage.size; + width = rawImage.width; + height = rawImage.height; + red_offset = rawImage.red_offset; + red_length = rawImage.red_length; + blue_offset = rawImage.blue_offset; + blue_length = rawImage.blue_length; + green_offset = rawImage.green_offset; + green_length = rawImage.green_length; + alpha_offset = rawImage.alpha_offset; + alpha_length = rawImage.alpha_length; + + data = rawImage.data; + } + + public RawImage toRawImage() { + RawImage rawImage = new RawImage(); + + rawImage.version = version; + rawImage.bpp = bpp; + rawImage.size = size; + rawImage.width = width; + rawImage.height = height; + rawImage.red_offset = red_offset; + rawImage.red_length = red_length; + rawImage.blue_offset = blue_offset; + rawImage.blue_length = blue_length; + rawImage.green_offset = green_offset; + rawImage.green_length = green_length; + rawImage.alpha_offset = alpha_offset; + rawImage.alpha_length = alpha_length; + + rawImage.data = data; + return rawImage; + } + } + + private static void writeOutImage(RawImage screenshot, String name) throws IOException { + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name)); + out.writeObject(new ChimpRunnerRawImage(screenshot)); + out.close(); + } + + public static void main(String[] args) throws IOException { + IChimpBackend backend = new AdbBackend(); + IChimpDevice device = backend.waitForConnection(); + IChimpImage snapshot = (IChimpImage) device.takeSnapshot(); + + // write out to a file + snapshot.writeToFile("output.png", "png"); + writeOutImage(((AdbChimpImage)snapshot).getRawImage(), "output.raw"); + System.exit(0); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java b/chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java new file mode 100644 index 0000000..39ec533 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/image/ImageUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Hashtable; +/** + * Useful image related functions. + */ +public class ImageUtils { + // Utility class + private ImageUtils() { } + + private static Hashtable<?,?> EMPTY_HASH = new Hashtable(); + private static int[] BAND_OFFSETS_32 = { 0, 1, 2, 3 }; + private static int[] BAND_OFFSETS_16 = { 0, 1 }; + + /** + * Convert a raw image into a buffered image. + * + * @param rawImage the raw image to convert + * @param image the old image to (possibly) recycle + * @return the converted image + */ + public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) { + switch (rawImage.bpp) { + case 16: + return rawImage16toARGB(image, rawImage); + case 32: + return rawImage32toARGB(rawImage); + } + return null; + } + + /** + * Convert a raw image into a buffered image. + * + * @param rawImage the image to convert. + * @return the converted image. + */ + public static BufferedImage convertImage(RawImage rawImage) { + return convertImage(rawImage, null); + } + + static int getMask(int length) { + int res = 0; + for (int i = 0 ; i < length ; i++) { + res = (res << 1) + 1; + } + + return res; + } + + private static BufferedImage rawImage32toARGB(RawImage rawImage) { + // Do as much as we can to not make an extra copy of the data. This is just a bunch of + // classes that wrap's the raw byte array of the image data. + DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); + + PixelInterleavedSampleModel sampleModel = + new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, + 4, rawImage.width * 4, BAND_OFFSETS_32); + WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, + new Point(0, 0)); + return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, false, EMPTY_HASH); + } + + private static BufferedImage rawImage16toARGB(BufferedImage image, RawImage rawImage) { + // Do as much as we can to not make an extra copy of the data. This is just a bunch of + // classes that wrap's the raw byte array of the image data. + DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); + + PixelInterleavedSampleModel sampleModel = + new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, + 2, rawImage.width * 2, BAND_OFFSETS_16); + WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, + new Point(0, 0)); + return new BufferedImage(new SixteenBitColorModel(rawImage), raster, false, EMPTY_HASH); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java b/chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java new file mode 100644 index 0000000..1a1fbd9 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/image/SixteenBitColorModel.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; + +/** + * Internal color model used to do conversion of 16bpp RawImages. + */ +class SixteenBitColorModel extends ColorModel { + private static final int[] BITS = { + 8, 8, 8, 8 + }; + public SixteenBitColorModel(RawImage rawImage) { + super(32 + , BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), + true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + } + + @Override + public boolean isCompatibleRaster(Raster raster) { + return true; + } + + private int getPixel(Object inData) { + byte[] data = (byte[]) inData; + int value = data[0] & 0x00FF; + value |= (data[1] << 8) & 0x0FF00; + + return value; + } + + @Override + public int getAlpha(Object inData) { + return 0xff; + } + + @Override + public int getBlue(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 0) & 0x01F) << 3; + } + + @Override + public int getGreen(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 5) & 0x03F) << 2; + } + + @Override + public int getRed(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 11) & 0x01F) << 3; + } + + @Override + public int getAlpha(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getBlue(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getGreen(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRed(int pixel) { + throw new UnsupportedOperationException(); + } +} diff --git a/chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java b/chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java new file mode 100644 index 0000000..dda43dc --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; + +/** + * Internal color model used to do conversion of 32bpp RawImages. + */ +class ThirtyTwoBitColorModel extends ColorModel { + private static final int[] BITS = { + 8, 8, 8, 8, + }; + private final int alphaLength; + private final int alphaMask; + private final int alphaOffset; + private final int blueMask; + private final int blueLength; + private final int blueOffset; + private final int greenMask; + private final int greenLength; + private final int greenOffset; + private final int redMask; + private final int redLength; + private final int redOffset; + + public ThirtyTwoBitColorModel(RawImage rawImage) { + super(32, BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), + true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + redOffset = rawImage.red_offset; + redLength = rawImage.red_length; + redMask = ImageUtils.getMask(redLength); + greenOffset = rawImage.green_offset; + greenLength = rawImage.green_length; + greenMask = ImageUtils.getMask(greenLength); + blueOffset = rawImage.blue_offset; + blueLength = rawImage.blue_length; + blueMask = ImageUtils.getMask(blueLength); + alphaLength = rawImage.alpha_length; + alphaOffset = rawImage.alpha_offset; + alphaMask = ImageUtils.getMask(alphaLength); + } + + @Override + public boolean isCompatibleRaster(Raster raster) { + return true; + } + + private int getPixel(Object inData) { + byte[] data = (byte[]) inData; + int value = data[0] & 0x00FF; + value |= (data[1] & 0x00FF) << 8; + value |= (data[2] & 0x00FF) << 16; + value |= (data[3] & 0x00FF) << 24; + + return value; + } + + @Override + public int getAlpha(Object inData) { + int pixel = getPixel(inData); + if(alphaLength == 0) { + return 0xff; + } + return ((pixel >>> alphaOffset) & alphaMask) << (8 - alphaLength); + } + + @Override + public int getBlue(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> blueOffset) & blueMask) << (8 - blueLength); + } + + @Override + public int getGreen(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> greenOffset) & greenMask) << (8 - greenLength); + } + + @Override + public int getRed(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> redOffset) & redMask) << (8 - redLength); + } + + @Override + public int getAlpha(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getBlue(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getGreen(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRed(int pixel) { + throw new UnsupportedOperationException(); + } +} diff --git a/chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java b/chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java new file mode 100644 index 0000000..e1ec29f --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/core/ChimpImageBase.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.core; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +/** + * Base class with basic functionality for ChimpImage implementations. + */ +public abstract class ChimpImageBase implements IChimpImage { + private static Logger LOG = Logger.getLogger(ChimpImageBase.class.getCanonicalName()); + + /** + * Convert the ChimpImage to a BufferedImage. + * + * @return a BufferedImage for this ChimpImage. + */ + @Override + public abstract BufferedImage createBufferedImage(); + + // Cache the BufferedImage so we don't have to generate it every time. + private WeakReference<BufferedImage> cachedBufferedImage = null; + + /** + * Utility method to handle getting the BufferedImage and managing the cache. + * + * @return the BufferedImage for this image. + */ + @Override + public BufferedImage getBufferedImage() { + // Check the cache first + if (cachedBufferedImage != null) { + BufferedImage img = cachedBufferedImage.get(); + if (img != null) { + return img; + } + } + + // Not in the cache, so create it and cache it. + BufferedImage img = createBufferedImage(); + cachedBufferedImage = new WeakReference<BufferedImage>(img); + return img; + } + + @Override + public byte[] convertToBytes(String format) { + BufferedImage argb = convertSnapshot(); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ImageIO.write(argb, format, os); + } catch (IOException e) { + return new byte[0]; + } + return os.toByteArray(); + } + + @Override + public boolean writeToFile(String path, String format) { + if (format != null) { + return writeToFileHelper(path, format); + } + int offset = path.lastIndexOf('.'); + if (offset < 0) { + return writeToFileHelper(path, "png"); + } + String ext = path.substring(offset + 1); + Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext); + if (!writers.hasNext()) { + return writeToFileHelper(path, "png"); + } + ImageWriter writer = writers.next(); + BufferedImage image = convertSnapshot(); + try { + File f = new File(path); + f.delete(); + + ImageOutputStream outputStream = ImageIO.createImageOutputStream(f); + writer.setOutput(outputStream); + + try { + writer.write(image); + } finally { + writer.dispose(); + outputStream.flush(); + } + } catch (IOException e) { + return false; + } + return true; + } + + @Override + public int getPixel(int x, int y) { + BufferedImage image = getBufferedImage(); + return image.getRGB(x, y); + } + + private BufferedImage convertSnapshot() { + BufferedImage image = getBufferedImage(); + + // Convert the image to ARGB so ImageIO writes it out nicely + BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics g = argb.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return argb; + } + + private boolean writeToFileHelper(String path, String format) { + BufferedImage argb = convertSnapshot(); + + try { + ImageIO.write(argb, format, new File(path)); + } catch (IOException e) { + return false; + } + return true; + } + + @Override + public boolean sameAs(IChimpImage other, double percent) { + BufferedImage otherImage = other.getBufferedImage(); + BufferedImage myImage = getBufferedImage(); + + // Easy size check + if (otherImage.getWidth() != myImage.getWidth()) { + return false; + } + if (otherImage.getHeight() != myImage.getHeight()) { + return false; + } + + int[] otherPixel = new int[1]; + int[] myPixel = new int[1]; + + int width = myImage.getWidth(); + int height = myImage.getHeight(); + + int numDiffPixels = 0; + // Now, go through pixel-by-pixel and check that the images are the same; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { + numDiffPixels++; + } + } + } + double numberPixels = (height * width); + double diffPercent = numDiffPixels / numberPixels; + return percent <= 1.0 - diffPercent; + } + + // TODO: figure out the location of this class and is superclasses + private static class BufferedImageChimpImage extends ChimpImageBase { + private final BufferedImage image; + + public BufferedImageChimpImage(BufferedImage image) { + this.image = image; + } + + @Override + public BufferedImage createBufferedImage() { + return image; + } + } + + public static IChimpImage loadImageFromFile(String path) { + File f = new File(path); + if (f.exists() && f.canRead()) { + try { + BufferedImage bufferedImage = ImageIO.read(new File(path)); + if (bufferedImage == null) { + LOG.log(Level.WARNING, "Cannot decode file %s", path); + return null; + } + return new BufferedImageChimpImage(bufferedImage); + } catch (IOException e) { + LOG.log(Level.WARNING, "Exception trying to decode image", e); + return null; + } + } else { + LOG.log(Level.WARNING, "Cannot read file %s", path); + return null; + } + } + + @Override + public IChimpImage getSubImage(int x, int y, int w, int h) { + BufferedImage image = getBufferedImage(); + return new BufferedImageChimpImage(image.getSubimage(x, y, w, h)); + } +} diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpBackend.java b/chimpchat/src/com/android/chimpchat/core/IChimpBackend.java new file mode 100644 index 0000000..092b849 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/core/IChimpBackend.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.core; + +import com.android.chimpchat.core.IChimpDevice; + +/** + * Interface between the ChimpChat API and the ChimpChat backend that communicates + * with Monkey. + */ +public interface IChimpBackend { + /** + * Wait for a default device to connect to the backend. + * + * @return the connected device (or null if timeout); + */ + IChimpDevice waitForConnection(); + + /** + * Wait for a device to connect to the backend. + * + * @param timeoutMs how long (in ms) to wait + * @param deviceIdRegex the regular expression to specify which device to wait for. + * @return the connected device (or null if timeout); + */ + IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex); + + /** + * Shutdown the backend and cleanup any resources it was using. + */ + void shutdown(); +} diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpDevice.java b/chimpchat/src/com/android/chimpchat/core/IChimpDevice.java new file mode 100644 index 0000000..7ba09c8 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/core/IChimpDevice.java @@ -0,0 +1,197 @@ + +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.core; + +import com.android.chimpchat.ChimpManager; +import com.android.chimpchat.hierarchyviewer.HierarchyViewer; + +import java.util.Collection; +import java.util.Map; + +import javax.annotation.Nullable; + +/** + * ChimpDevice interface. + */ +public interface IChimpDevice { + /** + * Create a ChimpManager for talking to this device. + * + * @return the ChimpManager + */ + ChimpManager getManager(); + + /** + * Dispose of any native resources this device may have taken hold of. + */ + void dispose(); + + /** + * @return hierarchy viewer implementation for querying state of the view + * hierarchy. + */ + HierarchyViewer getHierarchyViewer(); + + /** + * Take the current screen's snapshot. + * @return the snapshot image + */ + IChimpImage takeSnapshot(); + + /** + * Reboot the device. + * + * @param into which bootloader to boot into. Null means default reboot. + */ + void reboot(@Nullable String into); + + /** + * Get device's property. + * + * @param key the property name + * @return the property value + */ + String getProperty(String key); + + /** + * Get system property. + * + * @param key the name of the system property + * @return the property value + */ + String getSystemProperty(String key); + + /** + * Perform a touch of the given type at (x,y). + * + * @param x the x coordinate + * @param y the y coordinate + * @param type the touch type + */ + void touch(int x, int y, TouchPressType type); + + /** + * Perform a press of a given type using a given key. + * + * TODO: define standard key names in a separate class or enum + * + * @param keyName the name of the key to use + * @param type the type of press to perform + */ + void press(String keyName, TouchPressType type); + + /** + * Perform a drag from one one location to another + * + * @param startx the x coordinate of the drag's starting point + * @param starty the y coordinate of the drag's starting point + * @param endx the x coordinate of the drag's end point + * @param endy the y coordinate of the drag's end point + * @param steps the number of steps to take when interpolating points + * @param ms the duration of the drag + */ + void drag(int startx, int starty, int endx, int endy, int steps, long ms); + + /** + * Type a given string. + * + * @param string the string to type + */ + void type(String string); + + /** + * Execute a shell command. + * + * @param cmd the command to execute + * @return the output of the command + */ + String shell(String cmd); + + /** + * Install a given package. + * + * @param path the path to the installation package + * @return true if success + */ + boolean installPackage(String path); + + /** + * Uninstall a given package. + * + * @param packageName the name of the package + * @return true if success + */ + boolean removePackage(String packageName); + + /** + * Start an activity. + * + * @param uri the URI for the Intent + * @param action the action for the Intent + * @param data the data URI for the Intent + * @param mimeType the mime type for the Intent + * @param categories the category names for the Intent + * @param extras the extras to add to the Intent + * @param component the component of the Intent + * @param flags the flags for the Intent + */ + void startActivity(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimeType, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + + /** + * Send a broadcast intent to the device. + * + * @param uri the URI for the Intent + * @param action the action for the Intent + * @param data the data URI for the Intent + * @param mimeType the mime type for the Intent + * @param categories the category names for the Intent + * @param extras the extras to add to the Intent + * @param component the component of the Intent + * @param flags the flags for the Intent + */ + void broadcastIntent(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimeType, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + + /** + * Run the specified package with instrumentation and return the output it + * generates. + * + * Use this to run a test package using InstrumentationTestRunner. + * + * @param packageName The class to run with instrumentation. The format is + * packageName/className. Use packageName to specify the Android package to + * run, and className to specify the class to run within that package. For + * test packages, this is usually testPackageName/InstrumentationTestRunner + * @param args a map of strings to objects containing the arguments to pass + * to this instrumentation. + * @return A map of strings to objects for the output from the package. + * For a test package, contains a single key-value pair: the key is 'stream' + * and the value is a string containing the test output. + */ + Map<String, Object> instrument(String packageName, + Map<String, Object> args); + + /** + * Wake up the screen on the device. + */ + void wake(); +} diff --git a/chimpchat/src/com/android/chimpchat/core/IChimpImage.java b/chimpchat/src/com/android/chimpchat/core/IChimpImage.java new file mode 100644 index 0000000..6cd8f53 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/core/IChimpImage.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.core; + +import java.awt.image.BufferedImage; + +/** + * ChimpImage interface. + * + * This interface defines an image representing a screen snapshot. + */ +public interface IChimpImage { + // TODO: add java docs + BufferedImage createBufferedImage(); + BufferedImage getBufferedImage(); + + IChimpImage getSubImage(int x, int y, int w, int h); + + byte[] convertToBytes(String format); + boolean writeToFile(String path, String format); + int getPixel(int x, int y); + boolean sameAs(IChimpImage other, double percent); +} diff --git a/chimpchat/src/com/android/chimpchat/core/TouchPressType.java b/chimpchat/src/com/android/chimpchat/core/TouchPressType.java new file mode 100644 index 0000000..e5b92b7 --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/core/TouchPressType.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.chimpchat.core; + +import java.util.HashMap; +import java.util.Map; + +/** + * TouchPressType enum contains valid input for the "touch" Monkey command. + * When passed as a string, the "identifier" value is used. + */ +public enum TouchPressType { + DOWN("down"), UP("up"), DOWN_AND_UP("downAndUp"); + + private static final Map<String,TouchPressType> identifierToEnum = + new HashMap<String,TouchPressType>(); + static { + for (TouchPressType type : values()) { + identifierToEnum.put(type.identifier, type); + } + } + + private String identifier; + + TouchPressType(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + public static TouchPressType fromIdentifier(String name) { + return identifierToEnum.get(name); + } +} diff --git a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java new file mode 100644 index 0000000..6ad98ad --- /dev/null +++ b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.chimpchat.hierarchyviewer; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.DeviceBridge; +import com.android.hierarchyviewerlib.device.ViewNode; +import com.android.hierarchyviewerlib.device.Window; + +import org.eclipse.swt.graphics.Point; + +/** + * Class for querying the view hierarchy of the device. + */ +public class HierarchyViewer { + public static final String TAG = "hierarchyviewer"; + + private IDevice mDevice; + + /** + * Constructs the hierarchy viewer for the specified device. + * + * @param device The Android device to connect to. + */ + public HierarchyViewer(IDevice device) { + this.mDevice = device; + setupViewServer(); + } + + private void setupViewServer() { + DeviceBridge.setupDeviceForward(mDevice); + if (!DeviceBridge.isViewServerRunning(mDevice)) { + if (!DeviceBridge.startViewServer(mDevice)) { + // TODO: Get rid of this delay. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + if (!DeviceBridge.startViewServer(mDevice)) { + Log.e(TAG, "Unable to debug device " + mDevice); + throw new RuntimeException("Could not connect to the view server"); + } + return; + } + } + DeviceBridge.loadViewServerInfo(mDevice); + } + + /** + * Find a view by id. + * + * @param id id for the view. + * @return view with the specified ID, or {@code null} if no view found. + */ + + public ViewNode findViewById(String id) { + ViewNode rootNode = DeviceBridge.loadWindowData( + new Window(mDevice, "", 0xffffffff)); + if (rootNode == null) { + throw new RuntimeException("Could not dump view"); + } + return findViewById(id, rootNode); + } + + /** + * Find a view by ID, starting from the given root node + * @param id ID of the view you're looking for + * @param rootNode the ViewNode at which to begin the traversal + * @return view with the specified ID, or {@code null} if no view found. + */ + + public ViewNode findViewById(String id, ViewNode rootNode) { + if (rootNode.id.equals(id)) { + return rootNode; + } + + for (ViewNode child : rootNode.children) { + ViewNode found = findViewById(id,child); + if (found != null) { + return found; + } + } + return null; + } + + /** + * Gets the window that currently receives the focus. + * + * @return name of the window that currently receives the focus. + */ + public String getFocusedWindowName() { + int id = DeviceBridge.getFocusedWindow(mDevice); + Window[] windows = DeviceBridge.loadWindows(mDevice); + for (Window w : windows) { + if (w.getHashCode() == id) + return w.getTitle(); + } + return null; + } + + /** + * Gets the absolute x/y position of the view node. + * + * @param node view node to find position of. + * @return point specifying the x/y position of the node. + */ + public static Point getAbsolutePositionOfView(ViewNode node) { + int x = node.left; + int y = node.top; + ViewNode p = node.parent; + while (p != null) { + x += p.left - p.scrollX; + y += p.top - p.scrollY; + p = p.parent; + } + return new Point(x, y); + } + + /** + * Gets the absolute x/y center of the specified view node. + * + * @param node view node to find position of. + * @return absolute x/y center of the specified view node. + */ + public static Point getAbsoluteCenterOfView(ViewNode node) { + Point point = getAbsolutePositionOfView(node); + return new Point( + point.x + (node.width / 2), point.y + (node.height / 2)); + } + + /** + * Gets the visibility of a given element. + * + * @param selector selector for the view. + * @return True if the element is visible. + */ + public boolean visible(ViewNode node) { + boolean ret = (node != null) + && node.namedProperties.containsKey("getVisibility()") + && "VISIBLE".equalsIgnoreCase( + node.namedProperties.get("getVisibility()").value); + return ret; + + } + + /** + * Gets the text of a given element. + * + * @param selector selector for the view. + * @return the text of the given element. + */ + public String getText(ViewNode node) { + if (node == null) { + throw new RuntimeException("Node not found"); + } + ViewNode.Property textProperty = node.namedProperties.get("text:mText"); + if (textProperty == null) { + throw new RuntimeException("No text property on node"); + } + return textProperty.value; + } +} |