diff --git a/Mobilyzer/project.properties b/Mobilyzer/project.properties index 06a801a..bf2469f 100644 --- a/Mobilyzer/project.properties +++ b/Mobilyzer/project.properties @@ -11,7 +11,7 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-18 +target=android-21 android.library=true -android.library.reference.1=../../../android-sdk-macosx/extras/google/google_play_services/libproject/google-play-services_lib -android.library.reference.2=../../ModifiedExoPlayerLibrary/library/src/main +android.library.reference.1=../../ExoPlayerLib +android.library.reference.2=../../google-play-services_lib diff --git a/Mobilyzer/src/com/mobilyzer/api/API.java b/Mobilyzer/src/com/mobilyzer/api/API.java index 5582737..c68934a 100644 --- a/Mobilyzer/src/com/mobilyzer/api/API.java +++ b/Mobilyzer/src/com/mobilyzer/api/API.java @@ -40,6 +40,8 @@ import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.measurements.DnsLookupTask; import com.mobilyzer.measurements.HttpTask; +import com.mobilyzer.measurements.PageLoadTimeTask; +import com.mobilyzer.measurements.PageLoadTimeTask.PageLoadTimeDesc; import com.mobilyzer.measurements.ParallelTask; import com.mobilyzer.measurements.PingTask; import com.mobilyzer.measurements.SequentialTask; @@ -54,6 +56,8 @@ import com.mobilyzer.measurements.TCPThroughputTask.TCPThroughputDesc; import com.mobilyzer.measurements.TracerouteTask.TracerouteDesc; import com.mobilyzer.measurements.UDPBurstTask.UDPBurstDesc; +import com.mobilyzer.measurements.VideoQoETask; +import com.mobilyzer.measurements.VideoQoETask.VideoQoEDesc; import com.mobilyzer.util.Logger; /** @@ -294,10 +298,14 @@ public MeasurementTask createTask( TaskType taskType, Date startTime task = new UDPBurstTask(new UDPBurstDesc(clientKey, startTime, endTime , intervalSec, count, priority, contextIntervalSec, params)); break; -// case PLT: -// task = new PageLoadTimeTask(new PageLoadTimeDesc(clientKey, startTime, endTime -// , intervalSec, count, priority, contextIntervalSec, params)); -// break; + case PLT: + task = new PageLoadTimeTask(new PageLoadTimeDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; + case VIDEOQOE: + task = new VideoQoETask(new VideoQoEDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; default: throw new MeasurementError("Undefined measurement type. Candidate: " + "DNSLOOKUP, HTTP, PING, TRACEROUTE, TCPTHROUGHPUT, UDPBURST"); diff --git a/Mobilyzer/src/com/mobilyzer/measurements/DnsLookupTask.java b/Mobilyzer/src/com/mobilyzer/measurements/DnsLookupTask.java index 27ebe31..d582ac2 100755 --- a/Mobilyzer/src/com/mobilyzer/measurements/DnsLookupTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/DnsLookupTask.java @@ -17,11 +17,17 @@ import android.os.Parcel; import android.os.Parcelable; +import java.io.IOException; import java.io.InvalidClassException; import java.net.InetAddress; +import java.net.SocketAddress; +import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.security.InvalidParameterException; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.List; import java.util.Map; import com.mobilyzer.Config; @@ -34,220 +40,464 @@ import com.mobilyzer.util.MeasurementJsonConvertor; import com.mobilyzer.util.PhoneUtils; +import org.xbill.DNS.*; + /** * Measures the DNS lookup time */ -public class DnsLookupTask extends MeasurementTask{ - // Type name for internal use - public static final String TYPE = "dns_lookup"; - // Human readable name for the task - public static final String DESCRIPTOR = "DNS lookup"; - - //Since it's very hard to calculate the data consumed by this task - // directly, we use a fixed value. This is on the high side. - public static final int AVG_DATA_USAGE_BYTE=2000; - - private long duration; - - /** - * The description of DNS lookup measurement - */ - public static class DnsLookupDesc extends MeasurementDesc { - public String target; - private String server; - - - public DnsLookupDesc(String key, Date startTime, Date endTime, - double intervalSec, long count, long priority, - int contextIntervalSec, Map params) { - super(DnsLookupTask.TYPE, key, startTime, endTime, intervalSec, count, - priority, contextIntervalSec, params); - initializeParams(params); - if (this.target == null || this.target.length() == 0) { - throw new InvalidParameterException("LookupDnsTask cannot be created" + - " due to null target string"); - } - } - - /* - * @see com.google.wireless.speed.speedometer.MeasurementDesc#getType() +public class DnsLookupTask extends MeasurementTask { + // Type name for internal use + public static final String TYPE = "dns_lookup"; + // Human readable name for the task + public static final String DESCRIPTOR = "DNS lookup"; + + //Since it's very hard to calculate the data consumed by this task + // directly, we use a fixed value. This is on the high side. + public static final int AVG_DATA_USAGE_BYTE = 2000; + + private long duration; + private boolean debug = true; + + /** + * The description of DNS lookup measurement */ + public static class DnsLookupDesc extends MeasurementDesc { + public String target; + public String server; + public String qclass; + public String qtype; + + + public DnsLookupDesc(String key, Date startTime, Date endTime, + double intervalSec, long count, long priority, + int contextIntervalSec, Map params) { + super(DnsLookupTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + initializeParams(params); + if (this.target == null || this.target.length() == 0) { + throw new InvalidParameterException("LookupDnsTask cannot " + + "be created due to null " + + "target string"); + } + } + + /* + * @see com.google.wireless.speed.speedometer.MeasurementDesc#getType() + */ + @Override + public String getType() { + return DnsLookupTask.TYPE; + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + + this.server = params.get("server"); + this.target = params.get("target"); + // make the lookup absolute if it isn't already + if (!this.target.endsWith(".")) { + this.target = this.target + "."; + } + + /* we are extending the DNS measurement to allow setting + * arbitrary query classes and types, but we want to maintain + * backwards compatibility. Therefore, we are going to default + * to a standard IPv4 query, qclass IN and qtype A + */ + if (params.containsKey("qclass")) { + this.qclass = params.get("qclass"); + } else { + this.qclass = "IN"; + } + + if (params.containsKey("qtype")) { + this.qtype = params.get("qtype"); + } else { + this.qtype = "A"; + } + + } + + protected DnsLookupDesc(Parcel in) { + super(in); + target = in.readString(); + server = in.readString(); + qclass = in.readString(); + qtype = in.readString(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DnsLookupDesc createFromParcel(Parcel in) { + return new DnsLookupDesc(in); + } + + public DnsLookupDesc[] newArray(int size) { + return new DnsLookupDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(target); + dest.writeString(server); + dest.writeString(qclass); + dest.writeString(qtype); + } + } + + private class DNSWrapper { + public boolean isValid; + public String rawOutput; + public Message response; + public int qid; + public int id; + public long respTime; + + public DNSWrapper(boolean isValid, byte[] rawOutput, Message response, + int qid, int id, long respTime) { + this.isValid = isValid; + this.rawOutput = rawOutput.toString(); + this.response = response; + this.qid = qid; + this.id = id; + this.respTime = respTime; + } + } + + + public DnsLookupTask(MeasurementDesc desc) { + super(new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, + desc.contextIntervalSec, desc.parameters)); + this.duration = Config.DEFAULT_DNS_TASK_DURATION; + } + + protected DnsLookupTask(Parcel in) { + super(in); + duration = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DnsLookupTask createFromParcel(Parcel in) { + return new DnsLookupTask(in); + } + + public DnsLookupTask[] newArray(int size) { + return new DnsLookupTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + } + + /** + * Returns a copy of the DnsLookupTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + DnsLookupDesc newDesc = new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); + return new DnsLookupTask(newDesc); + } + + public ArrayList measureDNS(String domain, String qtype, String qclass) { + Record question = null; + try { + question = Record.newRecord(Name.fromString(domain), + Type.value(qtype), + DClass.value(qclass)); + } catch (TextParseException e) { + Logger.d("dns testing: Error constructing packet"); + } + if (debug) Logger.d("dns testing: constructed question"); + + Message query = Message.newQuery(question); + // wait for at most 5 seconds for a response + //long endTime = System.currentTimeMillis() + 5; + if (debug) Logger.d("dns testing: constructed query"); + ArrayList responses = sendMeasurement(query, false); + return responses; + } + + private ArrayList sendMeasurement(Message query, boolean useTCP) { + // now that we have a message, put it on the wire and wait for + // responses + int qid = query.getHeader().getID(); + byte[] output = query.toWire(); + int udpSize = SimpleResolver.maxUDPSize(query); + DnsLookupDesc desc = (DnsLookupDesc) this.measurementDesc; + long endTime = System.currentTimeMillis() + 60 * 5 * 1000; + + /* the people who wrote the DNS code were not awesome and didn't have abstract methods, + * so the code doesn't let me use their superclass, client. Therefore, I'm doing the hacky + * solution and creating 2 different clients + */ + TCPClient tclient = null; + UDPClient uclient = null; + if (useTCP || (output.length > udpSize)) { + try { + tclient = new TCPClient(endTime); + SocketAddress addr = new InetSocketAddress(desc.server, 53); + tclient.connect(addr); + useTCP = true; + } catch (IOException e) { + Logger.d("dns testing: Error creating client"); + } + + } else { + try { + uclient = new UDPClient(endTime); + uclient.bind(null); + SocketAddress addr = new InetSocketAddress(desc.server, 53); + uclient.connect(addr); + } catch (IOException e) { + Logger.e("dns testing: Error creating client"); + } + } + if (debug) Logger.d("dns testing: initialized client"); + + boolean shouldSend = true; + long startTime = 0; + long respTime; + ArrayList responses = new ArrayList(); + if (debug) Logger.d("dns testing: about to start loop current time " + System.currentTimeMillis() + " end time: " + endTime); + while (System.currentTimeMillis() < endTime) { + byte[] in = {}; + + if (debug) Logger.d("dns testing: in send loop"); + if (shouldSend) { + try { + if (useTCP) tclient.send(output); + else uclient.send(output); + startTime = System.currentTimeMillis(); + shouldSend = false; + } catch (IOException e) { + Logger.e("dns testing: Error sending"); + } + if (debug) Logger.d("dns testing: sent query"); + } + + try { + if (useTCP) { + in = tclient.recv(); + } else { + in = uclient.recv(udpSize); + } + } catch (IOException e) { + Logger.d("dns testing: Problem receiving packet due to " + e.getMessage()); + } + + if (debug) Logger.d("dns testing: received"); + + respTime = System.currentTimeMillis() - startTime; + + // if we didn't get anything back, then continue. this + // means we will break out if we are over time + if (in.length == 0) { + if (debug) Logger.d("dns testing: empty response, breaking out"); + break; + } + + DNSWrapper wrap; + Message response; + // don't parse the message if it's too short + if (in.length < Header.LENGTH) { + wrap = new DNSWrapper(false, in, null, qid, -1, respTime); + responses.add(wrap); + if (debug) Logger.d("dns testing: nothing to parse"); + continue; + } + + + int id = ((in[0] & 0xFF) << 8) + (in[1] & 0xFF); + try { + response = SimpleResolver.parseMessage(in); + wrap = new DNSWrapper(true, in, response, qid, id, respTime); + responses.add(wrap); + if (debug) Logger.d("dns testing: successfully parsed response"); + } catch (WireParseException e) { + Logger.e("dns testing: Problem trying to parse dns packet"); + wrap = new DNSWrapper(false, in, null, qid, -1, respTime); + responses.add(wrap); + continue; + } + + // if the response was truncated, then requery over TCP + if (!useTCP && response.getHeader().getFlag(Flags.TC)) { + try { + uclient.cleanup(); + tclient = new TCPClient(endTime); + SocketAddress addr = new InetSocketAddress(desc.server, 53); + tclient.connect(addr); + useTCP = true; + shouldSend = true; + if (debug) Logger.d("dns testing: requerying over tcp"); + } catch (IOException e) { + Logger.e("dns testing: Problem trying to retry over TCP"); + } + } + + } + + if (debug) Logger.d("dns testing: outside send loop"); + return responses; + } + + + @Override + public MeasurementResult[] call() throws MeasurementError { + ArrayList responses = null; + DnsLookupDesc desc = (DnsLookupDesc) this.measurementDesc; + for (int i = 0; i < Config.DEFAULT_DNS_COUNT_PER_MEASUREMENT; i++) { + DnsLookupDesc taskDesc = (DnsLookupDesc) this.measurementDesc; + Logger.i("Running DNS Lookup for target " + taskDesc.target); + responses = measureDNS(taskDesc.target, taskDesc.qtype, taskDesc.qclass); + } + if ((responses == null) || (responses.size() == 0)) { + throw new MeasurementError("Problems conducting DNS measurement"); + } else { + Logger.i("Successfully resolved target address"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + ArrayList results = new ArrayList(); + MeasurementResult result; +// for (DNSWrapper wrap : responses) { + result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + DnsLookupTask.TYPE, + System.currentTimeMillis() * 1000, + TaskProgress.COMPLETED, this.measurementDesc); + + // now turn the result into an array of hashmaps with the data we care about + + List> data = extractResults(responses); + result.addResult("results", data); + result.addResult("target", desc.target); + result.addResult("qtype", desc.qtype); + result.addResult("qclass", desc.qclass); + + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + results.add(result); +// } + + // create the result array to return + MeasurementResult resultsFinal [] = new MeasurementResult[results.size()]; + for (int i = 0; i < resultsFinal.length; i++) { + resultsFinal[i] = results.get(i); + } + + return resultsFinal; + } + } + + public List> extractResults(ArrayList responses) { + ArrayList> data = new ArrayList>(); + for (DNSWrapper wrap : responses) { + Message resp = null; + if (wrap.isValid) { + resp = wrap.response; + } + HashMap item = new HashMap(); + item.put("qryId", wrap.qid); + item.put("respId", wrap.id); + item.put("payload", wrap.rawOutput); + item.put("respTime", wrap.respTime); + item.put("isValid", wrap.isValid); + item.put("rcode", Rcode.string(resp.header.getRcode())); + item.put("tc", resp.getHeader().getFlag(Flags.TC)); + + // process the question + Record[] questionRecs = resp.getSectionArray(0); + if (questionRecs.length == 0) { + item.put("domain", null); + item.put("qtype", null); + item.put("qclass", null); + } else { + Record rec = questionRecs[0]; + item.put("domain", rec.name.toString()); + item.put("qtype", Type.string(rec.type)); + item.put("qclass", DClass.string(rec.dclass)); + } + + // now process the answers + List> answers = new ArrayList>(); + questionRecs = resp.getSectionArray(1); + for (Record recd : questionRecs) { + HashMap entry = new HashMap(); + entry.put("name", recd.name.toString()); + entry.put("rtype", Type.string(recd.type)); + entry.put("rdata", recd.rrToString()); + answers.add(entry); + } + item.put("answers", answers.toArray()); + data.add(item); + } + return data; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return DnsLookupDesc.class; + } + @Override public String getType() { - return DnsLookupTask.TYPE; + return DnsLookupTask.TYPE; } @Override - protected void initializeParams(Map params) { - if (params == null) { - return; - } + public String getDescriptor() { + return DESCRIPTOR; + } - this.target = params.get("target"); - this.server = params.get("server"); + @Override + public String toString() { + DnsLookupDesc desc = (DnsLookupDesc) measurementDesc; + return "[DNS Lookup]\n Target: " + desc.target + "\n Interval (sec): " + + desc.intervalSec + "\n Next run: " + desc.startTime; } - protected DnsLookupDesc(Parcel in) { - super(in); - target = in.readString(); - server = in.readString(); + @Override + public boolean stop() { + //There is nothing we need to do to stop the DNS measurement + return false; } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public DnsLookupDesc createFromParcel(Parcel in) { - return new DnsLookupDesc(in); - } + @Override + public long getDuration() { + return this.duration; + } - public DnsLookupDesc[] newArray(int size) { - return new DnsLookupDesc[size]; - } - }; @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeString(target); - dest.writeString(server); - } - } - - public DnsLookupTask(MeasurementDesc desc) { - super(new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, - desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, - desc.parameters)); - this.duration=Config.DEFAULT_DNS_TASK_DURATION; - } - - protected DnsLookupTask(Parcel in) { - super(in); - duration = in.readLong(); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public DnsLookupTask createFromParcel(Parcel in) { - return new DnsLookupTask(in); - } - - public DnsLookupTask[] newArray(int size) { - return new DnsLookupTask[size]; - } - }; - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeLong(duration); - } - /** - * Returns a copy of the DnsLookupTask - */ - @Override - public MeasurementTask clone() { - MeasurementDesc desc = this.measurementDesc; - DnsLookupDesc newDesc = new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, - desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); - return new DnsLookupTask(newDesc); - } - - @Override - public MeasurementResult[] call() throws MeasurementError { - long t1, t2; - long totalTime = 0; - InetAddress resultInet = null; - int successCnt = 0; - for (int i = 0; i < Config.DEFAULT_DNS_COUNT_PER_MEASUREMENT; i++) { - try { - DnsLookupDesc taskDesc = (DnsLookupDesc) this.measurementDesc; - Logger.i("Running DNS Lookup for target " + taskDesc.target); - t1 = System.currentTimeMillis(); - InetAddress inet = InetAddress.getByName(taskDesc.target); - t2 = System.currentTimeMillis(); - if (inet != null) { - totalTime += (t2 - t1); - resultInet = inet; - successCnt++; + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; } - } catch (UnknownHostException e) { - throw new MeasurementError("Cannot resovle domain name"); - } - } - - if (resultInet != null) { - Logger.i("Successfully resolved target address"); - PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); - MeasurementResult result = new MeasurementResult( - phoneUtils.getDeviceInfo().deviceId, - phoneUtils.getDeviceProperty(this.getKey()), - DnsLookupTask.TYPE, System.currentTimeMillis() * 1000, - TaskProgress.COMPLETED, this.measurementDesc); - result.addResult("address", resultInet.getHostAddress()); - result.addResult("real_hostname", resultInet.getCanonicalHostName()); - result.addResult("time_ms", totalTime / successCnt); - Logger.i(MeasurementJsonConvertor.toJsonString(result)); - MeasurementResult[] mrArray= new MeasurementResult[1]; - mrArray[0]=result; - return mrArray; - - } else { - throw new MeasurementError("Cannot resovle domain name"); - } - } - - @SuppressWarnings("rawtypes") - public static Class getDescClass() throws InvalidClassException { - return DnsLookupDesc.class; - } - - @Override - public String getType() { - return DnsLookupTask.TYPE; - } - - @Override - public String getDescriptor() { - return DESCRIPTOR; - } - - @Override - public String toString() { - DnsLookupDesc desc = (DnsLookupDesc) measurementDesc; - return "[DNS Lookup]\n Target: " + desc.target + "\n Interval (sec): " - + desc.intervalSec + "\n Next run: " + desc.startTime; - } - - @Override - public boolean stop() { - //There is nothing we need to do to stop the DNS measurement - 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; - } - } - - /** - * Since it is hard to get the amount of data sent directly, - * use a fixed value. The data consumed is usually small, and the fixed - * value is a conservative estimate. - * - * TODO find a better way to get this value - */ - @Override - public long getDataConsumed() { - return AVG_DATA_USAGE_BYTE; - } + } + + /** + * Since it is hard to get the amount of data sent directly, + * use a fixed value. The data consumed is usually small, and the fixed + * value is a conservative estimate. + *

+ * TODO find a better way to get this value + */ + @Override + public long getDataConsumed() { + return AVG_DATA_USAGE_BYTE; + } }