public final class EmulatorConsole {

    private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$

    private final static int WAIT_TIME = 5; // spin-wait sleep, in ms

    private final static int STD_TIMEOUT = 5000; // standard delay, in ms

    private final static String HOST = "127.0.0.1";  //$NON-NLS-1$

    private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
    private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
    private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
    private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
    private final static String COMMAND_GPS = "geo fix %1$f %2$f %3$f\r\n"; //$NON-NLS-1$

    private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$

    /**
     * Array of delay values: no delay, gprs, edge/egprs, umts/3d
     */
    public final static int[] MIN_LATENCIES = new int[] {
        0,      // No delay
        150,    // gprs
        80,     // edge/egprs
        35      // umts/3g
    };

    /**
     * Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa.
     */
    public final int[] DOWNLOAD_SPEEDS = new int[] {
        0,          // full speed
        14400,      // gsm
        43200,      // hscsd
        80000,      // gprs
        236800,     // edge/egprs
        1920000,    // umts/3g
        14400000    // hsdpa
    };

    /** Arrays of valid network speeds */
    public final static String[] NETWORK_SPEEDS = new String[] {
        "full", //$NON-NLS-1$
        "gsm", //$NON-NLS-1$
        "hscsd", //$NON-NLS-1$
        "gprs", //$NON-NLS-1$
        "edge", //$NON-NLS-1$
        "umts", //$NON-NLS-1$
        "hsdpa", //$NON-NLS-1$
    };

    /** Arrays of valid network latencies */
    public final static String[] NETWORK_LATENCIES = new String[] {
        "none", //$NON-NLS-1$
        "gprs", //$NON-NLS-1$
        "edge", //$NON-NLS-1$
        "umts", //$NON-NLS-1$
    };

    /** Gsm Mode enum. */
    public static enum GsmMode {
        UNKNOWN((String)null),
        UNREGISTERED(new String[] { "unregistered", "off" }),
        HOME(new String[] { "home", "on" }),
        ROAMING("roaming"),
        SEARCHING("searching"),
        DENIED("denied");

        private final String[] tags;

        GsmMode(String tag) {
            if (tag != null) {
                this.tags = new String[] { tag };
            } else {
                this.tags = new String[0];
            }
        }

        GsmMode(String[] tags) {
            this.tags = tags;
        }

        public static GsmMode getEnum(String tag) {
            for (GsmMode mode : values()) {
                for (String t : mode.tags) {
                    if (t.equals(tag)) {
                        return mode;
                    }
                }
            }
            return UNKNOWN;
        }

        /**
         * Returns the first tag of the enum.
         */
        public String getTag() {
            if (tags.length > 0) {
                return tags[0];
            }
            return null;
        }
    }

    public final static String RESULT_OK = null;

    private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
    private final static Pattern sVoiceStatusRegexp = Pattern.compile(
            "gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
    private final static Pattern sDataStatusRegexp = Pattern.compile(
            "gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
    private final static Pattern sDownloadSpeedRegexp = Pattern.compile(
            "\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
    private final static Pattern sMinLatencyRegexp = Pattern.compile(
            "\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$

    private final static HashMap<Integer, EmulatorConsole> sEmulators =
        new HashMap<Integer, EmulatorConsole>();

    /** Gsm Status class */
    public static class GsmStatus {
        /** Voice status. */
        public GsmMode voice = GsmMode.UNKNOWN;
        /** Data status. */
        public GsmMode data = GsmMode.UNKNOWN;
    }

    /** Network Status class */
    public static class NetworkStatus {
        /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
        public int speed = -1;
        /** network latency status.  This is an index in the {@link #MIN_LATENCIES} array. */
        public int latency = -1;
    }

    private int mPort;

    private SocketChannel mSocketChannel;

    private byte[] mBuffer = new byte[1024];

    /**
     * Returns an {@link EmulatorConsole} object for the given {@link Device}. This can
     * be an already existing console, or a new one if it hadn't been created yet.
     * @param d The device that the console links to.
     * @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed.
     */
    public static synchronized EmulatorConsole getConsole(IDevice d) {
        // we need to make sure that the device is an emulator
        // get the port number. This is the console port.
        Integer port = getEmulatorPort(d.getSerialNumber());
        if (port == null) {
            return null;
        }

        EmulatorConsole console = sEmulators.get(port);

        if (console != null) {
            // if the console exist, we ping the emulator to check the connection.
            if (console.ping() == false) {
                RemoveConsole(console.mPort);
                console = null;
            }
        }

        if (console == null) {
            // no console object exists for this port so we create one, and start
            // the connection.
            console = new EmulatorConsole(port);
            if (console.start()) {
                sEmulators.put(port, console);
            } else {
                console = null;
            }
        }

        return console;
    }

    /**
     * Return port of emulator given its serial number.
     *
     * @param serialNumber the emulator's serial number
     * @return the integer port or <code>null</code> if it could not be determined
     */
    public static Integer getEmulatorPort(String serialNumber) {
        Matcher m = sEmulatorRegexp.matcher(serialNumber);
        if (m.matches()) {
            // get the port number. This is the console port.
            int port;
            try {
                port = Integer.parseInt(m.group(1));
                if (port > 0) {
                    return port;
                }
            } catch (NumberFormatException e) {
                // looks like we failed to get the port number. This is a bit strange since
                // it's coming from a regexp that only accept digit, but we handle the case
                // and return null.
            }
        }
        return null;
    }

    /**
     * Removes the console object associated with a port from the map.
     * @param port The port of the console to remove.
     */
    private static synchronized void RemoveConsole(int port) {
        sEmulators.remove(port);
    }

    private EmulatorConsole(int port) {
        super();
        mPort = port;
    }

    /**
     * Starts the connection of the console.
     * @return true if success.
     */
    private boolean start() {

        InetSocketAddress socketAddr;
        try {
            InetAddress hostAddr = InetAddress.getByName(HOST);
            socketAddr = new InetSocketAddress(hostAddr, mPort);
        } catch (UnknownHostException e) {
            return false;
        }

        try {
            mSocketChannel = SocketChannel.open(socketAddr);
        } catch (IOException e1) {
            return false;
        }

        // read some stuff from it
        readLines();

        return true;
    }

    /**
     * Ping the emulator to check if the connection is still alive.
     * @return true if the connection is alive.
     */
    private synchronized boolean ping() {
        // it looks like we can send stuff, even when the emulator quit, but we can't read
        // from the socket. So we check the return of readLines()
        if (sendCommand(COMMAND_PING)) {
            return readLines() != null;
        }

        return false;
    }

    /**
     * Sends a KILL command to the emulator.
     */
    public synchronized void kill() {
        if (sendCommand(COMMAND_KILL)) {
            RemoveConsole(mPort);
        }
    }

    public synchronized String getAvdName() {
        if (sendCommand(COMMAND_AVD_NAME)) {
            String[] result = readLines();
            if (result != null && result.length == 2) { // this should be the name on first line,
                                                        // and ok on 2nd line
                return result[0];
            } else {
                // try to see if there's a message after KO
                Matcher m = RE_KO.matcher(result[result.length-1]);
                if (m.matches()) {
                    return m.group(1);
                }
            }
        }

        return null;
    }

    /**
     * Get the network status of the emulator.
     * @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
     * <code>null</code> if the query failed.
     */
    public synchronized NetworkStatus getNetworkStatus() {
        if (sendCommand(COMMAND_NETWORK_STATUS)) {
            /* Result is in the format
                Current network status:
                download speed:      14400 bits/s (1.8 KB/s)
                upload speed:        14400 bits/s (1.8 KB/s)
                minimum latency:  0 ms
                maximum latency:  0 ms
             */
            String[] result = readLines();

            if (isValid(result)) {
                // we only compare agains the min latency and the download speed
                // let's not rely on the order of the output, and simply loop through
                // the line testing the regexp.
                NetworkStatus status = new NetworkStatus();
                for (String line : result) {
                    Matcher m = sDownloadSpeedRegexp.matcher(line);
                    if (m.matches()) {
                        // get the string value
                        String value = m.group(1);

                        // get the index from the list
                        status.speed = getSpeedIndex(value);

                        // move on to next line.
                        continue;
                    }

                    m = sMinLatencyRegexp.matcher(line);
                    if (m.matches()) {
                        // get the string value
                        String value = m.group(1);

                        // get the index from the list
                        status.latency = getLatencyIndex(value);

                        // move on to next line.
                        continue;
                    }
                }

                return status;
            }
        }

        return null;
    }

    /**
     * Returns the current gsm status of the emulator
     * @return a {@link GsmStatus} object containing the gms status, or <code>null</code>
     * if the query failed.
     */
    public synchronized GsmStatus getGsmStatus() {
        if (sendCommand(COMMAND_GSM_STATUS)) {
            /*
             * result is in the format:
             * gsm status
             * gsm voice state: home
             * gsm data state:  home
             */

            String[] result = readLines();
            if (isValid(result)) {

                GsmStatus status = new GsmStatus();

                // let's not rely on the order of the output, and simply loop through
                // the line testing the regexp.
                for (String line : result) {
                    Matcher m = sVoiceStatusRegexp.matcher(line);
                    if (m.matches()) {
                        // get the string value
                        String value = m.group(1);

                        // get the index from the list
                        status.voice = GsmMode.getEnum(value.toLowerCase(Locale.US));

                        // move on to next line.
                        continue;
                    }

                    m = sDataStatusRegexp.matcher(line);
                    if (m.matches()) {
                        // get the string value
                        String value = m.group(1);

                        // get the index from the list
                        status.data = GsmMode.getEnum(value.toLowerCase(Locale.US));

                        // move on to next line.
                        continue;
                    }
                }

                return status;
            }
        }

        return null;
    }

    /**
     * Sets the GSM voice mode.
     * @param mode the {@link GsmMode} value.
     * @return RESULT_OK if success, an error String otherwise.
     * @throws InvalidParameterException if mode is an invalid value.
     */
    public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException {
        if (mode == GsmMode.UNKNOWN) {
            throw new InvalidParameterException();
        }

        String command = String.format(COMMAND_GSM_VOICE, mode.getTag());
        return processCommand(command);
    }

    /**
     * Sets the GSM data mode.
     * @param mode the {@link GsmMode} value
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     * @throws InvalidParameterException if mode is an invalid value.
     */
    public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException {
        if (mode == GsmMode.UNKNOWN) {
            throw new InvalidParameterException();
        }

        String command = String.format(COMMAND_GSM_DATA, mode.getTag());
        return processCommand(command);
    }

    /**
     * Initiate an incoming call on the emulator.
     * @param number a string representing the calling number.
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     */
    public synchronized String call(String number) {
        String command = String.format(COMMAND_GSM_CALL, number);
        return processCommand(command);
    }

    /**
     * Cancels a current call.
     * @param number the number of the call to cancel
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     */
    public synchronized String cancelCall(String number) {
        String command = String.format(COMMAND_GSM_CANCEL_CALL, number);
        return processCommand(command);
    }

    /**
     * Sends an SMS to the emulator
     * @param number The sender phone number
     * @param message The SMS message. \ characters must be escaped. The carriage return is
     * the 2 character sequence  {'\', 'n' }
     *
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     */
    public synchronized String sendSms(String number, String message) {
        String command = String.format(COMMAND_SMS_SEND, number, message);
        return processCommand(command);
    }

    /**
     * Sets the network speed.
     * @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table.
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     */
    public synchronized String setNetworkSpeed(int selectionIndex) {
        String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]);
        return processCommand(command);
    }

    /**
     * Sets the network latency.
     * @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table.
     * @return {@link #RESULT_OK} if success, an error String otherwise.
     */
    public synchronized String setNetworkLatency(int selectionIndex) {
        String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]);
        return processCommand(command);
    }

    public synchronized String sendLocation(double longitude, double latitude, double elevation) {

        // need to make sure the string format uses dot and not comma
        Formatter formatter = new Formatter(Locale.US);
        formatter.format(COMMAND_GPS, longitude, latitude, elevation);

        return processCommand(formatter.toString());
    }

    /**
     * Sends a command to the emulator console.
     * @param command The command string. <b>MUST BE TERMINATED BY \n</b>.
     * @return true if success
     */
    private boolean sendCommand(String command) {
        boolean result = false;
        try {
            byte[] bCommand;
            try {
                bCommand = command.getBytes(DEFAULT_ENCODING);
            } catch (UnsupportedEncodingException e) {
                // wrong encoding...
                return result;
            }

            // write the command
            AdbHelper.write(mSocketChannel, bCommand, bCommand.length, DdmPreferences.getTimeOut());

            result = true;
        } catch (Exception e) {
            return false;
        } finally {
            if (result == false) {
                // FIXME connection failed somehow, we need to disconnect the console.
                RemoveConsole(mPort);
            }
        }

        return result;
    }

    /**
     * Sends a command to the emulator and parses its answer.
     * @param command the command to send.
     * @return {@link #RESULT_OK} if success, an error message otherwise.
     */
    private String processCommand(String command) {
        if (sendCommand(command)) {
            String[] result = readLines();

            if (result != null && result.length > 0) {
                Matcher m = RE_KO.matcher(result[result.length-1]);
                if (m.matches()) {
                    return m.group(1);
                }
                return RESULT_OK;
            }

            return "Unable to communicate with the emulator";
        }

        return "Unable to send command to the emulator";
    }

    /**
     * Reads line from the console socket. This call is blocking until we read the lines:
     * <ul>
     * <li>OK\r\n</li>
     * <li>KO<msg>\r\n</li>
     * </ul>
     * @return the array of strings read from the emulator.
     */
    private String[] readLines() {
        try {
            ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length);
            int numWaits = 0;
            boolean stop = false;

            while (buf.position() != buf.limit() && stop == false) {
                int count;

                count = mSocketChannel.read(buf);
                if (count < 0) {
                    return null;
                } else if (count == 0) {
                    if (numWaits * WAIT_TIME > STD_TIMEOUT) {
                        return null;
                    }
                    // non-blocking spin
                    try {
                        Thread.sleep(WAIT_TIME);
                    } catch (InterruptedException ie) {
                    }
                    numWaits++;
                } else {
                    numWaits = 0;
                }

                // check the last few char aren't OK. For a valid message to test
                // we need at least 4 bytes (OK/KO + \r\n)
                if (buf.position() >= 4) {
                    int pos = buf.position();
                    if (endsWithOK(pos) || lastLineIsKO(pos)) {
                        stop = true;
                    }
                }
            }

            String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING);
            return msg.split("\r\n"); //$NON-NLS-1$
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * Returns true if the 4 characters *before* the current position are "OK\r\n"
     * @param currentPosition The current position
     */
    private boolean endsWithOK(int currentPosition) {
        if (mBuffer[currentPosition-1] == '\n' &&
                mBuffer[currentPosition-2] == '\r' &&
                mBuffer[currentPosition-3] == 'K' &&
                mBuffer[currentPosition-4] == 'O') {
            return true;
        }

        return false;
    }

    /**
     * Returns true if the last line starts with KO and is also terminated by \r\n
     * @param currentPosition the current position
     */
    private boolean lastLineIsKO(int currentPosition) {
        // first check that the last 2 characters are CRLF
        if (mBuffer[currentPosition-1] != '\n' ||
                mBuffer[currentPosition-2] != '\r') {
            return false;
        }

        // now loop backward looking for the previous CRLF, or the beginning of the buffer
        int i = 0;
        for (i = currentPosition-3 ; i >= 0; i--) {
            if (mBuffer[i] == '\n') {
                // found \n!
                if (i > 0 && mBuffer[i-1] == '\r') {
                    // found \r!
                    break;
                }
            }
        }

        // here it is either -1 if we reached the start of the buffer without finding
        // a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2
        if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') {
            // found error!
            return true;
        }

        return false;
    }

    /**
     * Returns true if the last line of the result does not start with KO
     */
    private boolean isValid(String[] result) {
        if (result != null && result.length > 0) {
            return !(RE_KO.matcher(result[result.length-1]).matches());
        }
        return false;
    }

    private int getLatencyIndex(String value) {
        try {
            // get the int value
            int latency = Integer.parseInt(value);

            // check for the speed from the index
            for (int i = 0 ; i < MIN_LATENCIES.length; i++) {
                if (MIN_LATENCIES[i] == latency) {
                    return i;
                }
            }
        } catch (NumberFormatException e) {
            // Do nothing, we'll just return -1.
        }

        return -1;
    }

    private int getSpeedIndex(String value) {
        try {
            // get the int value
            int speed = Integer.parseInt(value);

            // check for the speed from the index
            for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) {
                if (DOWNLOAD_SPEEDS[i] == speed) {
                    return i;
                }
            }
        } catch (NumberFormatException e) {
            // Do nothing, we'll just return -1.
        }

        return -1;
    }
} public static enum GsmMode {
    UNKNOWN((String)null),
    UNREGISTERED(new String[] { "unregistered", "off" }),
    HOME(new String[] { "home", "on" }),
    ROAMING("roaming"),
    SEARCHING("searching"),
    DENIED("denied");

    private final String[] tags;

    GsmMode(String tag) {
        if (tag != null) {
            this.tags = new String[] { tag };
        } else {
            this.tags = new String[0];
        }
    }

    GsmMode(String[] tags) {
        this.tags = tags;
    }

    public static GsmMode getEnum(String tag) {
        for (GsmMode mode : values()) {
            for (String t : mode.tags) {
                if (t.equals(tag)) {
                    return mode;
                }
            }
        }
        return UNKNOWN;
    }

    /**
     * Returns the first tag of the enum.
     */
    public String getTag() {
        if (tags.length > 0) {
            return tags[0];
        }
        return null;
    }
} public static class GsmStatus {
    /** Voice status. */
    public GsmMode voice = GsmMode.UNKNOWN;
    /** Data status. */
    public GsmMode data = GsmMode.UNKNOWN;
} public static class NetworkStatus {
    /** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
    public int speed = -1;
    /** network latency status.  This is an index in the {@link #MIN_LATENCIES} array. */
    public int latency = -1;
} public interface IDevice {

    public final static String PROP_BUILD_VERSION = "ro.build.version.release";
    public final static String PROP_BUILD_API_LEVEL = "ro.build.version.sdk";
    public final static String PROP_BUILD_CODENAME = "ro.build.version.codename";

    public final static String PROP_DEBUGGABLE = "ro.debuggable";

    /** Serial number of the first connected emulator. */
    public final static String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$
    /** Device change bit mask: {@link DeviceState} change. */
    public static final int CHANGE_STATE = 0x0001;
    /** Device change bit mask: {@link Client} list change. */
    public static final int CHANGE_CLIENT_LIST = 0x0002;
    /** Device change bit mask: build info change. */
    public static final int CHANGE_BUILD_INFO = 0x0004;

    /** @deprecated Use {@link #PROP_BUILD_API_LEVEL}. */
    @Deprecated
    public final static String PROP_BUILD_VERSION_NUMBER = PROP_BUILD_API_LEVEL;

    public final static String MNT_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; //$NON-NLS-1$
    public final static String MNT_ROOT = "ANDROID_ROOT"; //$NON-NLS-1$
    public final static String MNT_DATA = "ANDROID_DATA"; //$NON-NLS-1$

    /**
     * The state of a device.
     */
    public static enum DeviceState {
        BOOTLOADER("bootloader"), //$NON-NLS-1$
        OFFLINE("offline"), //$NON-NLS-1$
        ONLINE("device"), //$NON-NLS-1$
        RECOVERY("recovery"); //$NON-NLS-1$

        private String mState;

        DeviceState(String state) {
            mState = state;
        }

        /**
         * Returns a {@link DeviceState} from the string returned by <code>adb devices</code>.
         *
         * @param state the device state.
         * @return a {@link DeviceState} object or <code>null</code> if the state is unknown.
         */
        public static DeviceState getState(String state) {
            for (DeviceState deviceState : values()) {
                if (deviceState.mState.equals(state)) {
                    return deviceState;
                }
            }
            return null;
        }
    }

    /**
     * Namespace of a Unix Domain Socket created on the device.
     */
    public static enum DeviceUnixSocketNamespace {
        ABSTRACT("localabstract"),      //$NON-NLS-1$
        FILESYSTEM("localfilesystem"),  //$NON-NLS-1$
        RESERVED("localreserved");      //$NON-NLS-1$

        private String mType;

        private DeviceUnixSocketNamespace(String type) {
            mType = type;
        }

        String getType() {
            return mType;
        }
    };

    /**
     * Returns the serial number of the device.
     */
    public String getSerialNumber();

    /**
     * Returns the name of the AVD the emulator is running.
     * <p/>This is only valid if {@link #isEmulator()} returns true.
     * <p/>If the emulator is not running any AVD (for instance it's running from an Android source
     * tree build), this method will return "<code>&lt;build&gt;</code>".
     *
     * @return the name of the AVD or <code>null</code> if there isn't any.
     */
    public String getAvdName();

    /**
     * Returns the state of the device.
     */
    public DeviceState getState();

    /**
     * Returns the device properties. It contains the whole output of 'getprop'
     */
    public Map<String, String> getProperties();

    /**
     * Returns the number of property for this device.
     */
    public int getPropertyCount();

    /**
     * Returns the cached property value.
     *
     * @param name the name of the value to return.
     * @return the value or <code>null</code> if the property does not exist or has not yet been
     * cached.
     */
    public String getProperty(String name);

    /**
     * Returns <code>true></code> if properties have been cached
     */
    public boolean arePropertiesSet();

    /**
     * A variant of {@link #getProperty(String)} that will attempt to retrieve the given
     * property from device directly, without using cache.
     *
     * @param name the name of the value to return.
     * @return the value or <code>null</code> if the property does not exist
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a
     *             given time.
     * @throws IOException in case of I/O error on the connection.
     */
    public String getPropertySync(String name) throws TimeoutException,
            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException;

    /**
     * A combination of {@link #getProperty(String)} and {@link #getPropertySync(String)} that
     * will attempt to retrieve the property from cache if available, and if not, will query the
     * device directly.
     *
     * @param name the name of the value to return.
     * @return the value or <code>null</code> if the property does not exist
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output for a
     *             given time.
     * @throws IOException in case of I/O error on the connection.
     */
    public String getPropertyCacheOrSync(String name) throws TimeoutException,
            AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException;

    /**
     * Returns a mount point.
     *
     * @param name the name of the mount point to return
     *
     * @see #MNT_EXTERNAL_STORAGE
     * @see #MNT_ROOT
     * @see #MNT_DATA
     */
    public String getMountPoint(String name);

    /**
     * Returns if the device is ready.
     *
     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#ONLINE}.
     */
    public boolean isOnline();

    /**
     * Returns <code>true</code> if the device is an emulator.
     */
    public boolean isEmulator();

    /**
     * Returns if the device is offline.
     *
     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#OFFLINE}.
     */
    public boolean isOffline();

    /**
     * Returns if the device is in bootloader mode.
     *
     * @return <code>true</code> if {@link #getState()} returns {@link DeviceState#BOOTLOADER}.
     */
    public boolean isBootLoader();

    /**
     * Returns whether the {@link Device} has {@link Client}s.
     */
    public boolean hasClients();

    /**
     * Returns the array of clients.
     */
    public Client[] getClients();

    /**
     * Returns a {@link Client} by its application name.
     *
     * @param applicationName the name of the application
     * @return the <code>Client</code> object or <code>null</code> if no match was found.
     */
    public Client getClient(String applicationName);

    /**
     * Returns a {@link SyncService} object to push / pull files to and from the device.
     *
     * @return <code>null</code> if the SyncService couldn't be created. This can happen if adb
     *            refuse to open the connection because the {@link IDevice} is invalid
     *            (or got disconnected).
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException if the connection with adb failed.
     */
    public SyncService getSyncService()
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Returns a {@link FileListingService} for this device.
     */
    public FileListingService getFileListingService();

    /**
     * Takes a screen shot of the device and returns it as a {@link RawImage}.
     *
     * @return the screenshot as a <code>RawImage</code> or <code>null</code> if something
     *            went wrong.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public RawImage getScreenshot() throws TimeoutException, AdbCommandRejectedException,
            IOException;

    /**
     * Executes a shell command on the device, and sends the result to a <var>receiver</var>
     * <p/>This is similar to calling
     * <code>executeShellCommand(command, receiver, DdmPreferences.getTimeOut())</code>.
     *
     * @param command the shell command to execute
     * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell
     *            command
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output
     *            for a given time.
     * @throws IOException in case of I/O error on the connection.
     *
     * @see #executeShellCommand(String, IShellOutputReceiver, int)
     * @see DdmPreferences#getTimeOut()
     */
    public void executeShellCommand(String command, IShellOutputReceiver receiver)
            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
            IOException;

    /**
     * Executes a shell command on the device, and sends the result to a <var>receiver</var>.
     * <p/><var>maxTimeToOutputResponse</var> is used as a maximum waiting time when expecting the
     * command output from the device.<br>
     * At any time, if the shell command does not output anything for a period longer than
     * <var>maxTimeToOutputResponse</var>, then the method will throw
     * {@link ShellCommandUnresponsiveException}.
     * <p/>For commands like log output, a <var>maxTimeToOutputResponse</var> value of 0, meaning
     * that the method will never throw and will block until the receiver's
     * {@link IShellOutputReceiver#isCancelled()} returns <code>true</code>, should be
     * used.
     *
     * @param command the shell command to execute
     * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell
     *            command
     * @param maxTimeToOutputResponse the maximum amount of time during which the command is allowed
     *            to not output any response. A value of 0 means the method will wait forever
     *            (until the <var>receiver</var> cancels the execution) for command output and
     *            never throw.
     * @throws TimeoutException in case of timeout on the connection when sending the command.
     * @throws AdbCommandRejectedException if adb rejects the command.
     * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
     *            for a period longer than <var>maxTimeToOutputResponse</var>.
     * @throws IOException in case of I/O error on the connection.
     *
     * @see DdmPreferences#getTimeOut()
     */
    public void executeShellCommand(String command, IShellOutputReceiver receiver,
            int maxTimeToOutputResponse)
            throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
            IOException;

    /**
     * Runs the event log service and outputs the event log to the {@link LogReceiver}.
     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
     * @param receiver the receiver to receive the event log entries.
     * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the
     * timeout happens during setup. Once logs start being received, no timeout will occur as it's
     * not possible to detect a difference between no log and timeout.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void runEventLogService(LogReceiver receiver)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Runs the log service for the given log and outputs the log to the {@link LogReceiver}.
     * <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
     *
     * @param logname the logname of the log to read from.
     * @param receiver the receiver to receive the event log entries.
     * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the
     *            timeout happens during setup. Once logs start being received, no timeout will
     *            occur as it's not possible to detect a difference between no log and timeout.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void runLogService(String logname, LogReceiver receiver)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Creates a port forwarding between a local and a remote port.
     *
     * @param localPort the local port to forward
     * @param remotePort the remote port.
     * @return <code>true</code> if success.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void createForward(int localPort, int remotePort)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Creates a port forwarding between a local TCP port and a remote Unix Domain Socket.
     *
     * @param localPort the local port to forward
     * @param remoteSocketName name of the unix domain socket created on the device
     * @param namespace namespace in which the unix domain socket was created
     * @return <code>true</code> if success.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void createForward(int localPort, String remoteSocketName,
            DeviceUnixSocketNamespace namespace)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Removes a port forwarding between a local and a remote port.
     *
     * @param localPort the local port to forward
     * @param remotePort the remote port.
     * @return <code>true</code> if success.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void removeForward(int localPort, int remotePort)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Removes an existing port forwarding between a local and a remote port.
     *
     * @param localPort the local port to forward
     * @param remoteSocketName the remote unix domain socket name.
     * @param namespace namespace in which the unix domain socket was created
     * @return <code>true</code> if success.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     */
    public void removeForward(int localPort, String remoteSocketName,
            DeviceUnixSocketNamespace namespace)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Returns the name of the client by pid or <code>null</code> if pid is unknown
     * @param pid the pid of the client.
     */
    public String getClientName(int pid);

    /**
     * Push a single file.
     * @param local the local filepath.
     * @param remote The remote filepath.
     *
     * @throws IOException in case of I/O error on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws TimeoutException in case of a timeout reading responses from the device.
     * @throws SyncException if file could not be pushed
     */
    public void pushFile(String local, String remote)
            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException;

    /**
     * Pulls a single file.
     *
     * @param remote the full path to the remote file
     * @param local The local destination.
     *
     * @throws IOException in case of an IO exception.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws TimeoutException in case of a timeout reading responses from the device.
     * @throws SyncException in case of a sync exception.
     */
    public void pullFile(String remote, String local)
            throws IOException, AdbCommandRejectedException, TimeoutException, SyncException;

    /**
     * Installs an Android application on device. This is a helper method that combines the
     * syncPackageToDevice, installRemotePackage, and removePackage steps
     *
     * @param packageFilePath the absolute file system path to file on local host to install
     * @param reinstall set to <code>true</code> if re-install of app should be performed
     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
     *            available options.
     * @return a {@link String} with an error code, or <code>null</code> if success.
     * @throws InstallException if the installation fails.
     */
    public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs)
            throws InstallException;

    /**
     * Pushes a file to device
     *
     * @param localFilePath the absolute path to file on local host
     * @return {@link String} destination path on device for file
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException in case of I/O error on the connection.
     * @throws SyncException if an error happens during the push of the package on the device.
     */
    public String syncPackageToDevice(String localFilePath)
            throws TimeoutException, AdbCommandRejectedException, IOException, SyncException;

    /**
     * Installs the application package that was pushed to a temporary location on the device.
     *
     * @param remoteFilePath absolute file path to package file on device
     * @param reinstall set to <code>true</code> if re-install of app should be performed
     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
     *            available options.
     * @throws InstallException if the installation fails.
     */
    public String installRemotePackage(String remoteFilePath, boolean reinstall,
            String... extraArgs) throws InstallException;

    /**
     * Removes a file from device.
     *
     * @param remoteFilePath path on device of file to remove
     * @throws InstallException if the installation fails.
     */
    public void removeRemotePackage(String remoteFilePath) throws InstallException;

    /**
     * Uninstalls an package from the device.
     *
     * @param packageName the Android application package name to uninstall
     * @return a {@link String} with an error code, or <code>null</code> if success.
     * @throws InstallException if the uninstallation fails.
     */
    public String uninstallPackage(String packageName) throws InstallException;

    /**
     * Reboot the device.
     *
     * @param into the bootloader name to reboot into, or null to just reboot the device.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException
     */
    public void reboot(String into)
            throws TimeoutException, AdbCommandRejectedException, IOException;

    /**
     * Return the device's battery level, from 0 to 100 percent.
     * <p/>
     * The battery level may be cached. Only queries the device for its
     * battery level if 5 minutes have expired since the last successful query.
     *
     * @return the battery level or <code>null</code> if it could not be retrieved
     */
    public Integer getBatteryLevel() throws TimeoutException,
            AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException;

    /**
     * Return the device's battery level, from 0 to 100 percent.
     * <p/>
     * The battery level may be cached. Only queries the device for its
     * battery level if <code>freshnessMs</code> ms have expired since the last successful query.
     *
     * @param freshnessMs
     * @return the battery level or <code>null</code> if it could not be retrieved
     * @throws ShellCommandUnresponsiveException
     */
    public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
            AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException;
    
    /**
     * If you want to talk to monkey directly, use this one.
     * @return
     */
    public IChimpDevice getChimpDevice();
    
    /**
     * Must be released after device disconnected
     */
    public void releaseChimpDevice();
    
    public String execRawCmd(String cmd) throws TimeoutException, AdbCommandRejectedException, IOException;
    
    public int execShellCmdWithExitCode(String cmd, StringBuilder response) throws TimeoutException, AdbCommandRejectedException, IOException;
} public final class CoordinateControls {
    private double mValue;
    private boolean mValueValidity = false;
    private Text mDecimalText;
    private Text mSexagesimalDegreeText;
    private Text mSexagesimalMinuteText;
    private Text mSexagesimalSecondText;
    private final DecimalFormat mDecimalFormat = new DecimalFormat();

    /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)}
     * is called. This is an int instead of a boolean to act as a counter. */
    private int mManualTextChange = 0;

    /**
     * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode.
     */
    private ModifyListener mSexagesimalListener = new ModifyListener() {
        @Override
        public void modifyText(ModifyEvent event) {
            if (mManualTextChange > 0) {
                return;
            }
            try {
                mValue = getValueFromSexagesimalControls();
                setValueIntoDecimalControl(mValue);
                mValueValidity = true;
            } catch (ParseException e) {
                // wrong format empty the decimal controls.
                mValueValidity = false;
                resetDecimalControls();
            }
        }
    };

    /**
     * Creates the {@link Text} control for the decimal display of the coordinate.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createDecimalText(Composite parent) {
        mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent event) {
                if (mManualTextChange > 0) {
                    return;
                }
                try {
                    mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue();
                    setValueIntoSexagesimalControl(mValue);
                    mValueValidity = true;
                } catch (ParseException e) {
                    // wrong format empty the sexagesimal controls.
                    mValueValidity = false;
                    resetSexagesimalControls();
                }
            }
        });
    }

    /**
     * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalDegreeText(Composite parent) {
        mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalMinuteText(Composite parent) {
        mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal
     * mode.
     * <p/>The control is expected to be placed in a Composite using a {@link GridLayout}.
     * @param parent The {@link Composite} parent of the control.
     */
    public void createSexagesimalSecondText(Composite parent) {
        mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$
    }

    /**
     * Sets the coordinate into the {@link Text} controls.
     * @param value the coordinate value to set.
     */
    public void setValue(double value) {
        mValue = value;
        mValueValidity = true;
        setValueIntoDecimalControl(value);
        setValueIntoSexagesimalControl(value);
    }

    /**
     * Returns whether the value in the control(s) is valid.
     */
    public boolean isValueValid() {
        return mValueValidity;
    }

    /**
     * Returns the current value set in the control(s).
     * <p/>This value can be erroneous, and a check with {@link #isValueValid()} should be performed
     * before any call to this method.
     */
    public double getValue() {
        return mValue;
    }

    /**
     * Enables or disables all the {@link Text} controls.
     * @param enabled the enabled state.
     */
    public void setEnabled(boolean enabled) {
        mDecimalText.setEnabled(enabled);
        mSexagesimalDegreeText.setEnabled(enabled);
        mSexagesimalMinuteText.setEnabled(enabled);
        mSexagesimalSecondText.setEnabled(enabled);
    }

    private void resetDecimalControls() {
        mManualTextChange++;
        mDecimalText.setText(""); //$NON-NLS-1$
        mManualTextChange--;
    }

    private void resetSexagesimalControls() {
        mManualTextChange++;
        mSexagesimalDegreeText.setText(""); //$NON-NLS-1$
        mSexagesimalMinuteText.setText(""); //$NON-NLS-1$
        mSexagesimalSecondText.setText(""); //$NON-NLS-1$
        mManualTextChange--;
    }

    /**
     * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener}
     * @param parent the parent {@link Composite}.
     * @param defaultString the default string to be used to compute the {@link Text} control
     * size hint.
     * @param listener the {@link ModifyListener} to be called when the {@link Text} control is
     * modified.
     */
    private Text createTextControl(Composite parent, String defaultString,
            ModifyListener listener) {
        // create the control
        Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE);

        // add the standard listener to it.
        text.addModifyListener(listener);

        // compute its size/
        mManualTextChange++;
        text.setText(defaultString);
        text.pack();
        Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
        text.setText(""); //$NON-NLS-1$
        mManualTextChange--;

        GridData gridData = new GridData();
        gridData.widthHint = size.x;
        text.setLayoutData(gridData);

        return text;
    }

    private double getValueFromSexagesimalControls() throws ParseException {
        double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue();
        double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue();
        double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue();

        boolean isPositive = (degrees >= 0.);
        degrees = Math.abs(degrees);

        double value = degrees + minutes / 60. + seconds / 3600.;
        return isPositive ? value : - value;
    }

    private void setValueIntoDecimalControl(double value) {
        mManualTextChange++;
        mDecimalText.setText(String.format("%.6f", value));
        mManualTextChange--;
    }

    private void setValueIntoSexagesimalControl(double value) {
        // get the sign and make the number positive no matter what.
        boolean isPositive = (value >= 0.);
        value = Math.abs(value);

        // get the degree
        double degrees = Math.floor(value);

        // get the minutes
        double minutes = Math.floor((value - degrees) * 60.);

        // get the seconds.
        double seconds = (value - degrees) * 3600. - minutes * 60.;

        mManualTextChange++;
        mSexagesimalDegreeText.setText(
                Integer.toString(isPositive ? (int)degrees : (int)- degrees));
        mSexagesimalMinuteText.setText(Integer.toString((int)minutes));
        mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$
        mManualTextChange--;
    }
} public class GpxParser {
    
    private final static String NS_GPX = "http://www.topografix.com/GPX/1/1";  //$NON-NLS-1$
        
    private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$
    private final static String NODE_TRACK = "trk"; //$NON-NLS-1$
    private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$
    private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$
    private final static String NODE_NAME = "name"; //$NON-NLS-1$
    private final static String NODE_TIME = "time"; //$NON-NLS-1$
    private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$
    private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$
    private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$
    private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$
    
    private static SAXParserFactory sParserFactory;
    
    static {
        sParserFactory = SAXParserFactory.newInstance();
        sParserFactory.setNamespaceAware(true);
    }

    private String mFileName;

    private GpxHandler mHandler;
    
    /** Pattern to parse time with optional sub-second precision, and optional
     * Z indicating the time is in UTC. */
    private final static Pattern ISO8601_TIME =
        Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$
    
    /**
     * Handler for the SAX parser.
     */
    private static class GpxHandler extends DefaultHandler {
        // --------- parsed data --------- 
        List<WayPoint> mWayPoints;
        List<Track> mTrackList;
        
        // --------- state for parsing --------- 
        Track mCurrentTrack;
        TrackPoint mCurrentTrackPoint;
        WayPoint mCurrentWayPoint;
        final StringBuilder mStringAccumulator = new StringBuilder();
        
        boolean mSuccess = true;

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            // we only care about the standard GPX nodes.
            try {
                if (NS_GPX.equals(uri)) {
                    if (NODE_WAYPOINT.equals(localName)) {
                        if (mWayPoints == null) {
                            mWayPoints = new ArrayList<WayPoint>();
                        }
                        
                        mWayPoints.add(mCurrentWayPoint = new WayPoint());
                        handleLocation(mCurrentWayPoint, attributes);
                    } else if (NODE_TRACK.equals(localName)) {
                        if (mTrackList == null) {
                            mTrackList = new ArrayList<Track>();
                        }
                        
                        mTrackList.add(mCurrentTrack = new Track());
                    } else if (NODE_TRACK_SEGMENT.equals(localName)) {
                        // for now we do nothing here. This will merge all the segments into
                        // a single TrackPoint list in the Track.
                    } else if (NODE_TRACK_POINT.equals(localName)) {
                        if (mCurrentTrack != null) {
                            mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint());
                            handleLocation(mCurrentTrackPoint, attributes);
                        }
                    }
                }
            } finally {
                // no matter the node, we empty the StringBuilder accumulator when we start
                // a new node.
                mStringAccumulator.setLength(0);
            }
        }

        /**
         * Processes new characters for the node content. The characters are simply stored,
         * and will be processed when {@link #endElement(String, String, String)} is called.
         */
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            mStringAccumulator.append(ch, start, length);
        }
        
        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (NS_GPX.equals(uri)) {
                if (NODE_WAYPOINT.equals(localName)) {
                    mCurrentWayPoint = null;
                } else if (NODE_TRACK.equals(localName)) {
                    mCurrentTrack = null;
                } else if (NODE_TRACK_POINT.equals(localName)) {
                    mCurrentTrackPoint = null;
                } else if (NODE_NAME.equals(localName)) {
                    if (mCurrentTrack != null) {
                        mCurrentTrack.setName(mStringAccumulator.toString());
                    } else if (mCurrentWayPoint != null) {
                        mCurrentWayPoint.setName(mStringAccumulator.toString());
                    }
                } else if (NODE_TIME.equals(localName)) {
                    if (mCurrentTrackPoint != null) {
                        mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString()));
                    }
                } else if (NODE_ELEVATION.equals(localName)) {
                    if (mCurrentTrackPoint != null) {
                        mCurrentTrackPoint.setElevation(
                                Double.parseDouble(mStringAccumulator.toString()));
                    } else if (mCurrentWayPoint != null) {
                        mCurrentWayPoint.setElevation(
                                Double.parseDouble(mStringAccumulator.toString()));
                    }
                } else if (NODE_DESCRIPTION.equals(localName)) {
                    if (mCurrentWayPoint != null) {
                        mCurrentWayPoint.setDescription(mStringAccumulator.toString());
                    }
                }
            }
        }

        @Override
        public void error(SAXParseException e) throws SAXException {
            mSuccess = false;
        }

        @Override
        public void fatalError(SAXParseException e) throws SAXException {
            mSuccess = false;
        }
        
        /**
         * Converts the string description of the time into milliseconds since epoch.
         * @param timeString the string data.
         * @return date in milliseconds.
         */
        private long computeTime(String timeString) {
            // Time looks like: 2008-04-05T19:24:50Z
            Matcher m = ISO8601_TIME.matcher(timeString);
            if (m.matches()) {
                // get the various elements and reconstruct time as a long.
                try {
                    int year = Integer.parseInt(m.group(1));
                    int month = Integer.parseInt(m.group(2));
                    int date = Integer.parseInt(m.group(3));
                    int hourOfDay = Integer.parseInt(m.group(4));
                    int minute = Integer.parseInt(m.group(5));
                    int second = Integer.parseInt(m.group(6));
                    
                    // handle the optional parameters.
                    int milliseconds = 0;

                    String subSecondGroup = m.group(7);
                    if (subSecondGroup != null) {
                        milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup));
                    }
                    
                    boolean utcTime = m.group(8) != null;

                    // now we convert into milliseconds since epoch.
                    Calendar c;
                    if (utcTime) {
                        c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
                    } else {
                        c = Calendar.getInstance();
                    }
                    
                    c.set(year, month, date, hourOfDay, minute, second);
                    
                    return c.getTimeInMillis() + milliseconds;
                } catch (NumberFormatException e) {
                    // format is invalid, we'll return -1 below.
                }
                
            }

            // invalid time!
            return -1;
        }
        
        /**
         * Handles the location attributes and store them into a {@link LocationPoint}.
         * @param locationNode the {@link LocationPoint} to receive the location data.
         * @param attributes the attributes from the XML node.
         */
        private void handleLocation(LocationPoint locationNode, Attributes attributes) {
            try {
                double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE));
                double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE));
                
                locationNode.setLocation(longitude, latitude);
            } catch (NumberFormatException e) {
                // wrong data, do nothing.
            }
        }

        WayPoint[] getWayPoints() {
            if (mWayPoints != null) {
                return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
            }

            return null;
        }

        Track[] getTracks() {
            if (mTrackList != null) {
                return mTrackList.toArray(new Track[mTrackList.size()]);
            }

            return null;
        }
        
        boolean getSuccess() {
            return mSuccess;
        }
    }

    /**
     * A GPS track.
     * <p/>A track is composed of a list of {@link TrackPoint} and optional name and comment.
     */
    public final static class Track {
        private String mName;
        private String mComment;
        private List<TrackPoint> mPoints = new ArrayList<TrackPoint>();

        void setName(String name) {
            mName = name;
        }
        
        public String getName() {
            return mName;
        }
        
        void setComment(String comment) {
            mComment = comment;
        }
        
        public String getComment() {
            return mComment;
        }
        
        void addPoint(TrackPoint trackPoint) {
            mPoints.add(trackPoint);
        }
        
        public TrackPoint[] getPoints() {
            return mPoints.toArray(new TrackPoint[mPoints.size()]);
        }
        
        public long getFirstPointTime() {
            if (mPoints.size() > 0) {
                return mPoints.get(0).getTime();
            }
            
            return -1;
        }

        public long getLastPointTime() {
            if (mPoints.size() > 0) {
                return mPoints.get(mPoints.size()-1).getTime();
            }
            
            return -1;
        }
        
        public int getPointCount() {
            return mPoints.size();
        }
    }
    
    /**
     * Creates a new GPX parser for a file specified by its full path.
     * @param fileName The full path of the GPX file to parse.
     */
    public GpxParser(String fileName) {
        mFileName = fileName;
    }

    /**
     * Parses the GPX file.
     * @return <code>true</code> if success.
     */
    public boolean parse() {
        try {
            SAXParser parser = sParserFactory.newSAXParser();

            mHandler = new GpxHandler();

            parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
            
            return mHandler.getSuccess();
        } catch (ParserConfigurationException e) {
        } catch (SAXException e) {
        } catch (IOException e) {
        } finally {
        }

        return false;
    }
    
    /**
     * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
     * if the parsing failed.
     */
    public WayPoint[] getWayPoints() {
        if (mHandler != null) {
            return mHandler.getWayPoints();
        }
        
        return null;
    }
    
    /**
     * Returns the parsed {@link Track} objects, or <code>null</code> if none were found (or
     * if the parsing failed.
     */
    public Track[] getTracks() {
        if (mHandler != null) {
            return mHandler.getTracks();
        }
        
        return null;
    }
} public final static class Track {
    private String mName;
    private String mComment;
    private List<TrackPoint> mPoints = new ArrayList<TrackPoint>();

    void setName(String name) {
        mName = name;
    }
        
    public String getName() {
        return mName;
    }
        
    void setComment(String comment) {
        mComment = comment;
    }
        
    public String getComment() {
        return mComment;
    }
        
    void addPoint(TrackPoint trackPoint) {
        mPoints.add(trackPoint);
    }
        
    public TrackPoint[] getPoints() {
        return mPoints.toArray(new TrackPoint[mPoints.size()]);
    }
        
    public long getFirstPointTime() {
        if (mPoints.size() > 0) {
            return mPoints.get(0).getTime();
        }
            
        return -1;
    }

    public long getLastPointTime() {
        if (mPoints.size() > 0) {
            return mPoints.get(mPoints.size()-1).getTime();
        }
            
        return -1;
    }
        
    public int getPointCount() {
        return mPoints.size();
    }
} public class KmlParser {
    
    private final static String NS_KML_2 = "http://earth.google.com/kml/2.";  //$NON-NLS-1$
        
    private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$
    private final static String NODE_NAME = "name"; //$NON-NLS-1$
    private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$
    
    private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?");
    
    private static SAXParserFactory sParserFactory;
    
    static {
        sParserFactory = SAXParserFactory.newInstance();
        sParserFactory.setNamespaceAware(true);
    }

    private String mFileName;

    private KmlHandler mHandler;
    
    /**
     * Handler for the SAX parser.
     */
    private static class KmlHandler extends DefaultHandler {
        // --------- parsed data --------- 
        List<WayPoint> mWayPoints;
        
        // --------- state for parsing --------- 
        WayPoint mCurrentWayPoint;
        final StringBuilder mStringAccumulator = new StringBuilder();

        boolean mSuccess = true;

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            // we only care about the standard GPX nodes.
            try {
                if (uri.startsWith(NS_KML_2)) {
                    if (NODE_PLACEMARK.equals(localName)) {
                        if (mWayPoints == null) {
                            mWayPoints = new ArrayList<WayPoint>();
                        }
                        
                        mWayPoints.add(mCurrentWayPoint = new WayPoint());
                    }
                }
            } finally {
                // no matter the node, we empty the StringBuilder accumulator when we start
                // a new node.
                mStringAccumulator.setLength(0);
            }
        }

        /**
         * Processes new characters for the node content. The characters are simply stored,
         * and will be processed when {@link #endElement(String, String, String)} is called.
         */
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            mStringAccumulator.append(ch, start, length);
        }
        
        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (uri.startsWith(NS_KML_2)) {
                if (NODE_PLACEMARK.equals(localName)) {
                    mCurrentWayPoint = null;
                } else if (NODE_NAME.equals(localName)) {
                    if (mCurrentWayPoint != null) {
                        mCurrentWayPoint.setName(mStringAccumulator.toString());
                    }
                } else if (NODE_COORDINATES.equals(localName)) {
                    if (mCurrentWayPoint != null) {
                        parseLocation(mCurrentWayPoint, mStringAccumulator.toString());
                    }
                }
            }
        }

        @Override
        public void error(SAXParseException e) throws SAXException {
            mSuccess = false;
        }

        @Override
        public void fatalError(SAXParseException e) throws SAXException {
            mSuccess = false;
        }
        
        /**
         * Parses the location string and store the information into a {@link LocationPoint}.
         * @param locationNode the {@link LocationPoint} to receive the location data.
         * @param location The string containing the location info.
         */
        private void parseLocation(LocationPoint locationNode, String location) {
            Matcher m = sLocationPattern.matcher(location);
            if (m.matches()) {
                try {
                    double longitude = Double.parseDouble(m.group(1));
                    double latitude = Double.parseDouble(m.group(2));
                    
                    locationNode.setLocation(longitude, latitude);
                    
                    if (m.groupCount() == 3) {
                        // looks like we have elevation data.
                        locationNode.setElevation(Double.parseDouble(m.group(3)));
                    }
                } catch (NumberFormatException e) {
                    // wrong data, do nothing.
                }
            }
        }
        
        WayPoint[] getWayPoints() {
            if (mWayPoints != null) {
                return mWayPoints.toArray(new WayPoint[mWayPoints.size()]);
            }

            return null;
        }

        boolean getSuccess() {
            return mSuccess;
        }
    }

    /**
     * Creates a new GPX parser for a file specified by its full path.
     * @param fileName The full path of the GPX file to parse.
     */
    public KmlParser(String fileName) {
        mFileName = fileName;
    }

    /**
     * Parses the GPX file.
     * @return <code>true</code> if success.
     */
    public boolean parse() {
        try {
            SAXParser parser = sParserFactory.newSAXParser();

            mHandler = new KmlHandler();

            parser.parse(new InputSource(new FileReader(mFileName)), mHandler);
            
            return mHandler.getSuccess();
        } catch (ParserConfigurationException e) {
        } catch (SAXException e) {
        } catch (IOException e) {
        } finally {
        }

        return false;
    }
    
    /**
     * Returns the parsed {@link WayPoint} objects, or <code>null</code> if none were found (or
     * if the parsing failed.
     */
    public WayPoint[] getWayPoints() {
        if (mHandler != null) {
            return mHandler.getWayPoints();
        }
        
        return null;
    }
} public class TrackContentProvider implements IStructuredContentProvider {

    @Override
    public Object[] getElements(Object inputElement) {
        if (inputElement instanceof Track[]) {
            return (Track[])inputElement;
        }

        return new Object[0];
    }

    @Override
    public void dispose() {
        // pass
    }

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        // pass
    }
} public class TrackLabelProvider implements ITableLabelProvider {

    @Override
    public Image getColumnImage(Object element, int columnIndex) {
        return null;
    }

    @Override
    public String getColumnText(Object element, int columnIndex) {
        if (element instanceof Track) {
            Track track = (Track)element;
            switch (columnIndex) {
                case 0:
                    return track.getName();
                case 1:
                    return Integer.toString(track.getPointCount());
                case 2:
                    long time = track.getFirstPointTime();
                    if (time != -1) {
                        return new Date(time).toString();
                    }
                    break;
                case 3:
                    time = track.getLastPointTime();
                    if (time != -1) {
                        return new Date(time).toString();
                    }
                    break;
                case 4:
                    return track.getComment();
            }
        }

        return null;
    }

    @Override
    public void addListener(ILabelProviderListener listener) {
        // pass
    }

    @Override
    public void dispose() {
        // pass
    }

    @Override
    public boolean isLabelProperty(Object element, String property) {
        // pass
        return false;
    }

    @Override
    public void removeListener(ILabelProviderListener listener) {
        // pass
    }
} public class TrackPoint extends LocationPoint {
    private long mTime;

    void setTime(long time) {
        mTime = time;
    }
    
    public long getTime() {
        return mTime;
    }
} public final class WayPoint extends LocationPoint {
    private String mName;
    private String mDescription;

    void setName(String name) {
        mName = name;
    }
    
    public String getName() {
        return mName;
    }

    void setDescription(String description) {
        mDescription = description;
    }

    public String getDescription() {
        return mDescription;
    }
} public class WayPointContentProvider implements IStructuredContentProvider {

    @Override
    public Object[] getElements(Object inputElement) {
        if (inputElement instanceof WayPoint[]) {
            return (WayPoint[])inputElement;
        }

        return new Object[0];
    }

    @Override
    public void dispose() {
        // pass
    }

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        // pass
    }
} public class WayPointLabelProvider implements ITableLabelProvider {

    @Override
    public Image getColumnImage(Object element, int columnIndex) {
        return null;
    }

    @Override
    public String getColumnText(Object element, int columnIndex) {
        if (element instanceof WayPoint) {
            WayPoint wayPoint = (WayPoint)element;
            switch (columnIndex) {
                case 0:
                    return wayPoint.getName();
                case 1:
                    return String.format("%.6f", wayPoint.getLongitude());
                case 2:
                    return String.format("%.6f", wayPoint.getLatitude());
                case 3:
                    if (wayPoint.hasElevation()) {
                        return String.format("%.1f", wayPoint.getElevation());
                    } else {
                        return "-";
                    }
                case 4:
                    return wayPoint.getDescription();
            }
        }

        return null;
    }

    @Override
    public void addListener(ILabelProviderListener listener) {
        // pass
    }

    @Override
    public void dispose() {
        // pass
    }

    @Override
    public boolean isLabelProperty(Object element, String property) {
        // pass
        return false;
    }

    @Override
    public void removeListener(ILabelProviderListener listener) {
        // pass
    }
}
import com.android.ddmlib.EmulatorConsole;
import com.android.ddmlib.EmulatorConsole.GsmMode;
import com.android.ddmlib.EmulatorConsole.GsmStatus;
import com.android.ddmlib.EmulatorConsole.NetworkStatus;
import com.android.ddmlib.IDevice;
import com.android.ddmuilib.location.CoordinateControls;
import com.android.ddmuilib.location.GpxParser;
import com.android.ddmuilib.location.GpxParser.Track;
import com.android.ddmuilib.location.KmlParser;
import com.android.ddmuilib.location.TrackContentProvider;
import com.android.ddmuilib.location.TrackLabelProvider;
import com.android.ddmuilib.location.TrackPoint;
import com.android.ddmuilib.location.WayPoint;
import com.android.ddmuilib.location.WayPointContentProvider;
import com.android.ddmuilib.location.WayPointLabelProvider;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;

        label = new Label(decimalContent, SWT.NONE);
        label.setText("Longitude");

        mLongitudeControls.createDecimalText(decimalContent);

        label = new Label(decimalContent, SWT.NONE);
        label.setText("Latitude");

        mLatitudeControls.createDecimalText(decimalContent);

        // sexagesimal content
        final Composite sexagesimalContent = new Composite(content, SWT.NONE);
        sexagesimalContent.setLayout(gl = new GridLayout(7, false));
        gl.marginHeight = gl.marginWidth = 0;

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("Longitude");

        mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\u00B0"); // degree character

        mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("'");

        mLongitudeControls.createSexagesimalSecondText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\"");

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("Latitude");

        mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\u00B0");

        mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("'");

        mLatitudeControls.createSexagesimalSecondText(sexagesimalContent);

        label = new Label(sexagesimalContent, SWT.NONE);
        label.setText("\"");

        // set the default display to decimal
        sl.topControl = decimalContent;
        mDecimalButton.setSelection(true);

        mDecimalButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mDecimalButton.getSelection()) {
                    sl.topControl = decimalContent;
                } else {
                    sl.topControl = sexagesimalContent;
                }
                content.layout();
            }
        });

        Button sendButton = new Button(manualLocationComp, SWT.PUSH);
        sendButton.setText("Send");
        sendButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (mEmulatorConsole != null) {
                    processCommandResult(mEmulatorConsole.sendLocation(
                            mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0));
                }
            }
        });

        mLongitudeControls.setValue(DEFAULT_LONGITUDE);
        mLatitudeControls.setValue(DEFAULT_LATITUDE);
    }

    private void createGpxLocationControl(Composite gpxLocationComp) {
        GridData gd;

        IPreferenceStore store = DdmUiPreferences.getStore();

        gpxLocationComp.setLayout(new GridLayout(1, false));

        mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH);
        mGpxUploadButton.setText("Load GPX...");

        // Table for way point
        mGpxWayPointTable = new Table(gpxLocationComp,
                SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
        mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
        gd.heightHint = 100;
        mGpxWayPointTable.setHeaderVisible(true);
        mGpxWayPointTable.setLinesVisible(true);

        TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT,
                "Some Name",
                PREFS_WAYPOINT_COL_NAME, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LONGITUDE, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT,
                "-199.999999",
                PREFS_WAYPOINT_COL_LATITUDE, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT,
                "99999.9",
                PREFS_WAYPOINT_COL_ELEVATION, store);
        TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT,
                "Some Description",
                PREFS_WAYPOINT_COL_DESCRIPTION, store);

        final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable);
        gpxWayPointViewer.setContentProvider(new WayPointContentProvider());
        gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider());