| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ddmlib; |
| |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.UnknownHostException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.SocketChannel; |
| import java.security.InvalidParameterException; |
| import java.util.Calendar; |
| import java.util.HashMap; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Provides control over emulated hardware of the Android emulator. |
| * <p/>This is basically a wrapper around the command line console normally used with telnet. |
| *<p/> |
| * Regarding line termination handling:<br> |
| * One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most |
| * implementations don't enforce it (the dos one does). In this particular case, this is mostly |
| * irrelevant since we don't use telnet in Java, but that means we want to make |
| * sure we use the same line termination than what the console expects. The console |
| * code removes <code>\r</code> and waits for <code>\n</code>. |
| * <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console. |
| * <p/> |
| * <b>This API will change in the near future.</b> |
| */ |
| 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 nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$ |
| "%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$ |
| "1,10,0.0,0.0,0,0.0,0,0.0,0000\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 |
| Matcher m = sEmulatorRegexp.matcher(d.getSerialNumber()); |
| 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 null; |
| } |
| } 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; |
| } |
| |
| 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 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()); |
| |
| // 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()); |
| |
| // 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) { |
| |
| Calendar c = Calendar.getInstance(); |
| |
| double absLong = Math.abs(longitude); |
| int longDegree = (int)Math.floor(absLong); |
| char longDirection = 'E'; |
| if (longitude < 0) { |
| longDirection = 'W'; |
| } |
| |
| double longMinute = (absLong - Math.floor(absLong)) * 60; |
| |
| double absLat = Math.abs(latitude); |
| int latDegree = (int)Math.floor(absLat); |
| char latDirection = 'N'; |
| if (latitude < 0) { |
| latDirection = 'S'; |
| } |
| |
| double latMinute = (absLat - Math.floor(absLat)) * 60; |
| |
| String command = String.format(COMMAND_GPS, |
| c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), |
| c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND), |
| latDegree, latMinute, latDirection, |
| longDegree, longMinute, longDirection); |
| |
| return processCommand(command); |
| } |
| |
| /** |
| * 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, AdbHelper.STD_TIMEOUT); |
| |
| result = true; |
| } catch (IOException 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; |
| } |
| } |