/* Copyright 2012 Google Inc.
*
* 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.mobilyzer.measurements;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.mobilyzer.Config;
import com.mobilyzer.MeasurementDesc;
import com.mobilyzer.MeasurementResult;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.MeasurementResult.TaskProgress;
import com.mobilyzer.exceptions.MeasurementError;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.Util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
/**
* A callable that executes a ping task using one of three methods
*/
public class PingTask extends MeasurementTask{
// Type name for internal use
public static final String TYPE = "ping";
// Human readable name for the task
public static final String DESCRIPTOR = "ping";
/* Default payload size of the ICMP packet, plus the 8-byte ICMP header resulting in a total of
* 64-byte ICMP packet */
public static final int DEFAULT_PING_PACKET_SIZE = 56;
public static final int DEFAULT_PING_TIMEOUT = 10;
public static final int DEFAULT_PING_TTL = 51;
private long duration;
private Process pingProc = null;
private String PING_METHOD_CMD = "ping_cmd";
private String PING_METHOD_JAVA = "java_ping";
private String PING_METHOD_HTTP = "http";
private String targetIp = null;
//Track data consumption for this task to avoid exceeding user's limit
private long dataConsumed;
/**
* Encode ping specific parameters, along with common parameters inherited from MeasurmentDesc
* @author wenjiezeng@google.com (Steve Zeng)
*
*/
public static class PingDesc extends MeasurementDesc {
public String pingExe = null;
// Host address either in the numeric form or domain names
public String target = null;
// The payload size in bytes of the ICMP packet
public int packetSizeByte = PingTask.DEFAULT_PING_PACKET_SIZE;
public int pingTimeoutSec = PingTask.DEFAULT_PING_TIMEOUT;
public int pingTimeToLive= PingTask.DEFAULT_PING_TTL;
public double pingIcmpIntervalSec= Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC;
public PingDesc(String key, Date startTime,
Date endTime, double intervalSec, long count, long priority, int contextIntervalSec,
Map<String, String> params) throws InvalidParameterException {
super(PingTask.TYPE, key, startTime, endTime, intervalSec, count,
priority, contextIntervalSec,params);
initializeParams(params);
if (this.target == null || this.target.length() == 0) {
throw new InvalidParameterException("PingTask cannot be created due "
+ " to null target string");
}
}
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
this.target = params.get("target");
try {
String val = null;
if ((val = params.get("packet_size_byte")) != null && val.length() > 0 &&
Integer.parseInt(val) > 0) {
this.packetSizeByte = Integer.parseInt(val);
}
if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0 &&
Integer.parseInt(val) > 0) {
this.pingTimeoutSec = Integer.parseInt(val);
}
if ((val = params.get("ttl")) != null && val.length() > 0 &&
Integer.parseInt(val) > 0) {
this.pingTimeToLive = Integer.parseInt(val);
}
if ((val = params.get("icmp_interval_sec")) != null && val.length() > 0 &&
Double.parseDouble(val) > 0) {
this.pingIcmpIntervalSec = Double.parseDouble(val);
}
} catch (NumberFormatException e) {
throw new InvalidParameterException("PingTask cannot be created due to invalid params");
}
}
@Override
public String getType() {
return PingTask.TYPE;
}
protected PingDesc(Parcel in) {
super(in);
pingExe = in.readString();
target = in.readString();
packetSizeByte = in.readInt();
pingTimeoutSec = in.readInt();
pingTimeToLive = in.readInt();
pingIcmpIntervalSec = in.readDouble();
}
public static final Parcelable.Creator<PingDesc> CREATOR
= new Parcelable.Creator<PingDesc>() {
public PingDesc createFromParcel(Parcel in) {
return new PingDesc(in);
}
public PingDesc[] newArray(int size) {
return new PingDesc[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(pingExe);
dest.writeString(target);
dest.writeInt(packetSizeByte);
dest.writeInt(pingTimeoutSec);
dest.writeInt(pingTimeToLive);
dest.writeDouble(pingIcmpIntervalSec);
}
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return PingDesc.class;
}
public PingTask(MeasurementDesc desc) {
super(new PingDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec,
desc.count, desc.priority, desc.contextIntervalSec, desc.parameters));
this.duration=Config.PING_COUNT_PER_MEASUREMENT*500;
// this.taskProgress=TaskProgress.FAILED;
// this.stopFlag=false;
this.dataConsumed=0;
}
protected PingTask(Parcel in) {
super(in);
duration = in.readLong();
dataConsumed = in.readLong();
}
public static final Parcelable.Creator<PingTask> CREATOR =
new Parcelable.Creator<PingTask>() {
public PingTask createFromParcel(Parcel in) {
return new PingTask(in);
}
public PingTask[] newArray(int size) {
return new PingTask[size];
}
};
@Override
public int describeContents() {
return super.describeContents();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(duration);
dest.writeLong(dataConsumed);
}
/**
* Returns a copy of the PingTask
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
PingDesc newDesc = new PingDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters);
return new PingTask(newDesc);
}
/* We will use three methods to ping the requested resource in the order of PING_COMMAND,
* JAVA_ICMP_PING, and HTTP_PING. If all fails, then we declare the resource unreachable */
@Override
public MeasurementResult[] call() throws MeasurementError {
MeasurementResult[] result = null;
PingDesc desc = (PingDesc) measurementDesc;
int ipByteLength;
try {
InetAddress addr = InetAddress.getByName(desc.target);
// Get the address length
ipByteLength = addr.getAddress().length;
Logger.i("IP address length is " + ipByteLength);
// All ping methods ping against targetIp rather than desc.target
targetIp = addr.getHostAddress();
Logger.i("IP is " + targetIp);
} catch (UnknownHostException e) {
throw new MeasurementError("Unknown host " + desc.target);
}
result=new MeasurementResult[1];
try {
Logger.i("running ping command");
// Prevents the phone from going to low-power mode where WiFi turns off
result[0]=executePingCmdTask(ipByteLength);
return result;
}
catch (MeasurementError e) {
try {
Logger.i("running java ping");
result[0]=executeJavaPingTask();
return result;
} catch (MeasurementError ee) {
Logger.i("running http ping");
result[0]=executeHttpPingTask();
return result;
}
}
}
@Override
public String getType() {
return PingTask.TYPE;
}
@Override
public String getDescriptor() {
return DESCRIPTOR;
}
private MeasurementResult constructResult(ArrayList<Double> rrtVals, double packetLoss,
int packetsSent, String pingMethod) {
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
double mdev, avg, filteredAvg;
double total = 0;
if (rrtVals.size() == 0) {
return null;
}
for (double rrt : rrtVals) {
if (rrt < min) {
min = rrt;
}
if (rrt > max) {
max = rrt;
}
total += rrt;
}
avg = total / rrtVals.size();
mdev = Util.getStandardDeviation(rrtVals, avg);
filteredAvg = filterPingResults(rrtVals, avg);
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(this.getKey()),
PingTask.TYPE, System.currentTimeMillis() * 1000,
TaskProgress.COMPLETED, this.measurementDesc);
result.addResult("target_ip", targetIp);
result.addResult("mean_rtt_ms", avg);
result.addResult("min_rtt_ms", min);
result.addResult("max_rtt_ms", max);
result.addResult("stddev_rtt_ms", mdev);
if (filteredAvg != avg) {
result.addResult("filtered_mean_rtt_ms", filteredAvg);
}
result.addResult("packet_loss", packetLoss);
result.addResult("packets_sent", packetsSent);
result.addResult("ping_method", pingMethod);
Logger.i(MeasurementJsonConvertor.toJsonString(result));
return result;
}
private void cleanUp(Process proc) {
try {
if (proc != null) {
proc.destroy();
}
} catch (Exception e) {
Logger.w("Unable to kill ping process" + e.getMessage());
}
}
/**
* Compute the average of the filtered rtts.
* The first several ping results are usually extremely large as the device
* needs to activate the wireless interface and resolve domain names.
* Such distorted measurements are filtered out
*/
private double filterPingResults(final ArrayList<Double> rrts, double avg) {
double rrtAvg = avg;
// Our # of results should be less than the # of times we ping
try {
ArrayList<Double> filteredResults = Util.applyInnerBandFilter(rrts,
Double.MIN_VALUE, rrtAvg * Config.PING_FILTER_THRES);
// Now we compute the average again based on the filtered results
if (filteredResults != null && filteredResults.size() > 0) {
rrtAvg = Util.getSum(filteredResults) / filteredResults.size();
}
} catch (InvalidParameterException e) {
Log.wtf("", "This should never happen because rrts is never empty");
}
return rrtAvg;
}
// Runs when SystemState is IDLE
private MeasurementResult executePingCmdTask(int ipByteLen)
throws MeasurementError {
Logger.i("Starting executePingCmdTask");
PingDesc pingTask = (PingDesc) this.measurementDesc;
String errorMsg = "";
MeasurementResult measurementResult = null;
// TODO(Wenjie): Add a exhaustive list of ping locations for different
// Android phones
pingTask.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen);
Logger.i("Ping executable is " + pingTask.pingExe);
if (pingTask.pingExe == null) {
Logger.e("Ping executable not found");
throw new MeasurementError("Ping executable not found");
}
try {
String command = Util.constructCommand(pingTask.pingExe, "-i",
pingTask.pingIcmpIntervalSec,
"-s", pingTask.packetSizeByte, "-w", pingTask.pingTimeoutSec, "-c",
Config.PING_COUNT_PER_MEASUREMENT, "-t" ,pingTask.pingTimeToLive, targetIp);
Logger.i("Running: " + command);
pingProc = Runtime.getRuntime().exec(command);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
// Grab the output of the process that runs the ping command
InputStream is = pingProc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
int lineCnt = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
ArrayList<Integer> receivedIcmpSeq = new ArrayList<Integer>();
double packetLoss = Double.MIN_VALUE;
int packetsSent = Config.PING_COUNT_PER_MEASUREMENT;
// Process each line of the ping output and store the rrt in array rrts.
while ((line = br.readLine()) != null) {
// Ping prints a number of 'param=value' pairs, among which we only need
// the 'time=rrt_val' pair
String[] extractedValues = Util.extractInfoFromPingOutput(line);
if (extractedValues != null) {
int curIcmpSeq = Integer.parseInt(extractedValues[0]);
double rrtVal = Double.parseDouble(extractedValues[1]);
// ICMP responses from the system ping command could be duplicate
// and out of order
if (!receivedIcmpSeq.contains(curIcmpSeq)) {
rrts.add(rrtVal);
receivedIcmpSeq.add(curIcmpSeq);
}
}
// Get the number of sent/received pings from the ping command output
int[] packetLossInfo = Util.extractPacketLossInfoFromPingOutput(line);
if (packetLossInfo != null) {
packetsSent = packetLossInfo[0];
int packetsReceived = packetLossInfo[1];
packetLoss = 1 - ((double) packetsReceived / (double) packetsSent);
}
Logger.i(line);
}
// Use the output from the ping command to compute packet loss. If that's not
// available, use an estimation.
if (packetLoss == Double.MIN_VALUE) {
packetLoss = 1 -
((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
}
measurementResult = constructResult(rrts, packetLoss,
packetsSent, PING_METHOD_CMD);
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (SecurityException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (NumberFormatException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (InvalidParameterException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} finally {
// All associated streams with the process will be closed upon destroy()
cleanUp(pingProc);
}
if (measurementResult == null) {
Logger.e("Error running ping: " + errorMsg);
throw new MeasurementError(errorMsg);
}
return measurementResult;
}
// Runs when the ping command fails
private MeasurementResult executeJavaPingTask() throws MeasurementError {
PingDesc pingTask = (PingDesc) this.measurementDesc;
long pingStartTime = 0;
long pingEndTime = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
String errorMsg = "";
MeasurementResult result = null;
try {
int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec /
Config.PING_COUNT_PER_MEASUREMENT);
int successfulPingCnt = 0;
long totalPingDelay = 0;
for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) {
pingStartTime = System.currentTimeMillis();
boolean status = InetAddress.getByName(targetIp).isReachable(timeOut);
pingEndTime = System.currentTimeMillis();
long rrtVal = pingEndTime - pingStartTime;
if (status) {
totalPingDelay += rrtVal;
rrts.add((double) rrtVal);
}
}
Logger.i("java ping succeeds");
double packetLoss = 1 -
((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
result = constructResult(rrts, packetLoss,
Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_JAVA);
} catch (IllegalArgumentException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
}
if (result != null) {
return result;
} else {
Logger.i("java ping fails");
throw new MeasurementError(errorMsg);
}
}
/**
* Use the HTTP Head method to emulate ping. The measurement from this method
* can be substantially (2x) greater than the first two methods and inaccurate.
* This is because, depending on the implementing of the destination web
* server, either a quick HTTP response is replied or some actual heavy
* lifting will be done in preparing the response
* */
private MeasurementResult executeHttpPingTask() throws MeasurementError {
long pingStartTime = 0;
long pingEndTime = 0;
ArrayList<Double> rrts = new ArrayList<Double>();
PingDesc pingTask = (PingDesc) this.measurementDesc;
String errorMsg = "";
MeasurementResult result = null;
try {
long totalPingDelay = 0;
URL url = new URL("http://"+ pingTask.target);
int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec /
Config.PING_COUNT_PER_MEASUREMENT);
for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) {
pingStartTime = System.currentTimeMillis();
HttpURLConnection httpClient = (HttpURLConnection) url.openConnection();
httpClient.setRequestProperty("Connection", "close");
httpClient.setRequestMethod("HEAD");
httpClient.setReadTimeout(timeOut);
httpClient.setConnectTimeout(timeOut);
httpClient.connect();
pingEndTime = System.currentTimeMillis();
httpClient.disconnect();
rrts.add((double) (pingEndTime - pingStartTime));
}
Logger.i("HTTP get ping succeeds");
Logger.i("RTT is " + rrts.toString());
double packetLoss = 1
- ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT);
dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2;
result = constructResult(rrts, packetLoss,
Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_HTTP);
} catch (MalformedURLException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
} catch (IOException e) {
Logger.e(e.getMessage());
errorMsg += e.getMessage() + "\n";
}
if (result != null) {
return result;
} else {
Logger.i("HTTP get ping fails");
throw new MeasurementError(errorMsg);
}
}
@Override
public String toString() {
PingDesc desc = (PingDesc) measurementDesc;
return "[Ping]\n Target: " + desc.target + "\n Interval (sec): "
+ desc.intervalSec + "\n Next run: " + desc.startTime;
}
@Override
public boolean stop() {
return false;
}
@Override
public long getDuration() {
return this.duration;
}
@Override
public void setDuration(long newDuration) {
if(newDuration<0){
this.duration=0;
}else{
this.duration=newDuration;
}
}
/**
* Data sent so far by this task.
*
* We count packets sent directly to calculate the data sent
*/
@Override
public long getDataConsumed() {
return dataConsumed;
}
}