de.clemensbartz.chattychimpchat.adb.AdbChimpDevice.java Source code

Java tutorial

Introduction

Here is the source code for de.clemensbartz.chattychimpchat.adb.AdbChimpDevice.java

Source

/*
 * 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 de.clemensbartz.chattychimpchat.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.annotations.Nullable;
import de.clemensbartz.chattychimpchat.ChimpManager;
import de.clemensbartz.chattychimpchat.adb.LinearInterpolator.Point;
import de.clemensbartz.chattychimpchat.core.IChimpImage;
import de.clemensbartz.chattychimpchat.core.IChimpDevice;
import de.clemensbartz.chattychimpchat.core.IChimpView;
import de.clemensbartz.chattychimpchat.core.IMultiSelector;
import de.clemensbartz.chattychimpchat.core.ISelector;
import de.clemensbartz.chattychimpchat.core.PhysicalButton;
import de.clemensbartz.chattychimpchat.core.TouchPressType;

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;

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)
            throws TimeoutException, IOException, AdbCommandRejectedException, InterruptedException {
        this.device = device;
        this.manager = createManager("127.0.0.1", 12345);

        Preconditions.checkNotNull(this.manager);
    }

    public ChimpManager getManager() {
        return manager;
    }

    public void dispose() throws IOException {
        manager.quit();
        manager.close();
        executor.shutdown();
        manager = null;
    }

    private void executeAsyncCommand(final String command, final LoggingOutputReceiver logger) {
        executor.submit(new Runnable() {
            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) throws TimeoutException,
            AdbCommandRejectedException, IOException, UnknownHostException, InterruptedException {
        device.createForward(port, port);

        String command = "monkey --port " + port;
        executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE));

        // Sleep for a second to give the command time to execute.
        Thread.sleep(1000);

        InetAddress addr = InetAddress.getByName(address);

        // 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;
            }

            Thread.sleep(MANAGER_CREATE_WAIT_TIME_MS);

            Socket monkeySocket = new Socket(addr, port);

            mm = new ChimpManager(monkeySocket);

            mm.wake();
            success = true;
        }

        return mm;
    }

    public IChimpImage takeSnapshot() throws TimeoutException, AdbCommandRejectedException, IOException {
        return new AdbChimpImage(device.getScreenshot());
    }

    public String getSystemProperty(String key) {
        return device.getProperty(key);
    }

    public String getProperty(String key) throws IOException {
        return manager.getVariable(key);
    }

    public Collection<String> getPropertyList() throws IOException {
        return manager.listVariable();
    }

    public void wake() throws IOException {
        manager.wake();
    }

    private String shell(String... args)
            throws TimeoutException, ShellCommandUnresponsiveException, AdbCommandRejectedException, IOException {
        StringBuilder cmd = new StringBuilder();
        for (String arg : args) {
            cmd.append(arg).append(" ");
        }
        return shell(cmd.toString());
    }

    public String shell(String cmd)
            throws TimeoutException, ShellCommandUnresponsiveException, AdbCommandRejectedException, IOException {
        // 5000 is the default timeout from the ddmlib.
        // This timeout arg is needed to the backwards compatibility.
        return shell(cmd, 5000);
    }

    public String shell(String cmd, int timeout)
            throws TimeoutException, ShellCommandUnresponsiveException, AdbCommandRejectedException, IOException {
        CommandOutputCapture capture = new CommandOutputCapture();
        device.executeShellCommand(cmd, capture, timeout);
        return capture.toString();
    }

    public boolean installPackage(String path) throws InstallException {
        String result = device.installPackage(path, true);
        if (result != null) {
            LOG.log(Level.SEVERE, "Got error installing package: " + result);
            return false;
        }
        return true;
    }

    public boolean removePackage(String packageName) throws InstallException {
        String result = device.uninstallPackage(packageName);
        if (result != null) {
            LOG.log(Level.SEVERE, "Got error uninstalling package " + packageName + ": " + result);
            return false;
        }
        return true;
    }

    public void press(String keyName, TouchPressType type) throws IOException {
        switch (type) {
        case DOWN_AND_UP:
            manager.press(keyName);
            break;
        case DOWN:
            manager.keyDown(keyName);
            break;
        case UP:
            manager.keyUp(keyName);
            break;
        }
    }

    public void press(PhysicalButton key, TouchPressType type) throws IOException {
        press(key.getKeyName(), type);
    }

    public void type(String string) throws IOException {
        manager.type(string);
    }

    public void touch(int x, int y, TouchPressType type) throws IOException {
        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;
        case MOVE:
            manager.touchMove(x, y);
            break;
        }
    }

    public void reboot(String into) throws TimeoutException, AdbCommandRejectedException, IOException {
        device.reboot(into);
    }

    public void startActivity(String uri, String action, String data, String mimetype,
            Collection<String> categories, Map<String, Object> extras, String component, int flags)
            throws IOException, InterruptedException, AdbCommandRejectedException, TimeoutException,
            ShellCommandUnresponsiveException {
        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));
    }

    public void broadcastIntent(String uri, String action, String data, String mimetype,
            Collection<String> categories, Map<String, Object> extras, String component, int flags)
            throws IOException, InterruptedException, AdbCommandRejectedException, TimeoutException,
            ShellCommandUnresponsiveException {
        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;
    }

    public Map<String, Object> instrument(String packageName, Map<String, Object> args) throws IOException,
            InterruptedException, AdbCommandRejectedException, TimeoutException, ShellCommandUnresponsiveException {
        List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r");
        for (Entry<String, Object> entry : args.entrySet()) {
            final String key = entry.getKey();
            final Object value = entry.getValue();
            if (key != null && value != null) {
                shellCmd.add("-e");
                shellCmd.add(key);
                shellCmd.add(value.toString());
            }
        }
        shellCmd.add(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;
    }

    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() {
            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);
                }
            }

            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);
                }
            }

            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);
                }
            }
        });
    }

    public Collection<String> getViewIdList() throws IOException {
        return manager.listViewIds();
    }

    public IChimpView getView(ISelector selector) {
        return selector.getView(manager);
    }

    public Collection<IChimpView> getViews(IMultiSelector selector) throws IOException {
        return selector.getViews(manager);
    }

    public IChimpView getRootView() throws IOException {
        return manager.getRootView();
    }
}