├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── measures
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ ├── io
│ │ └── apisense
│ │ │ └── network
│ │ │ ├── dns
│ │ │ ├── TruncatedException.java
│ │ │ ├── DNSRecord.java
│ │ │ ├── DNSLookupResult.java
│ │ │ ├── DNSLookupConfig.java
│ │ │ └── DNSLookupTask.java
│ │ │ ├── MLabListener.java
│ │ │ ├── MeasurementConfigException.java
│ │ │ ├── MeasurementCallback.java
│ │ │ ├── MeasurementError.java
│ │ │ ├── MeasurementExecutor.java
│ │ │ ├── ping
│ │ │ ├── Rtt.java
│ │ │ ├── ICMPConfig.java
│ │ │ ├── TracerouteConfig.java
│ │ │ ├── TracerouteResult.java
│ │ │ ├── ICMPResult.java
│ │ │ ├── TracerouteTask.java
│ │ │ └── ICMPTask.java
│ │ │ ├── Measurement.java
│ │ │ ├── MeasurementResult.java
│ │ │ ├── udp
│ │ │ ├── UDPBurstResult.java
│ │ │ ├── UDPBurstTask.java
│ │ │ ├── MetricCalculator.java
│ │ │ ├── UDPDownloadBurstTask.java
│ │ │ ├── UDPBurstConfig.java
│ │ │ ├── UDPUploadBurstTask.java
│ │ │ └── UDPPacket.java
│ │ │ ├── tcp
│ │ │ ├── TCPThroughputResult.java
│ │ │ ├── TCPDownloadTask.java
│ │ │ ├── TCPThroughputConfig.java
│ │ │ ├── TCPThroughputTask.java
│ │ │ └── TCPUploadTask.java
│ │ │ └── MLabNS.java
│ │ └── org
│ │ └── xbill
│ │ └── DNS
│ │ ├── DNSClient.java
│ │ ├── PublicUDPClient.java
│ │ └── PublicTCPClient.java
├── proguard-rules.pro
└── build.gradle
├── .gitignore
├── nexusConfig.gradle
├── README.md
├── gradlew.bat
├── gradlew
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':measures'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/APISENSE/android-network-measures/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/measures/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/dns/TruncatedException.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.dns;
2 |
3 | /**
4 | * This exception indicated that the DNS query was over UDP
5 | * and the response is truncated.
6 | *
7 | * The query then will have to be made again by TCP.
8 | */
9 | class TruncatedException extends Exception {
10 | }
11 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MLabListener.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | import java.util.List;
4 |
5 | /**
6 | * Handle the callback from {@link MLabNS} task.
7 | */
8 | public interface MLabListener {
9 | /**
10 | * Execute this method when available servers are found.
11 | *
12 | * @param ips The list of available servers
13 | */
14 | void onMLabFinished(List ips);
15 | }
16 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MeasurementConfigException.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | /**
4 | * Exception to throw when constraints are not satisfied in a measurement config
5 | */
6 |
7 | public class MeasurementConfigException extends Exception {
8 | public MeasurementConfigException(String reason) {
9 | super(reason);
10 | }
11 |
12 | public MeasurementConfigException(Exception cause) {
13 | super(cause);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/measures/src/main/java/org/xbill/DNS/DNSClient.java:
--------------------------------------------------------------------------------
1 | package org.xbill.DNS;
2 |
3 | import java.io.IOException;
4 | import java.net.SocketAddress;
5 |
6 | /**
7 | * Common interface to use UDP and TCP clients.
8 | */
9 | public interface DNSClient {
10 | void bind(SocketAddress addr) throws IOException;
11 |
12 | void connect(SocketAddress addr) throws IOException;
13 |
14 | void send(byte[] data) throws IOException;
15 |
16 | byte[] recv(int max) throws IOException;
17 |
18 | void cleanup() throws IOException;
19 | }
20 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MeasurementCallback.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | /**
4 | * Defines the behavior to process on Measurement finished,
5 | * either by a success or an error.
6 | */
7 | public interface MeasurementCallback {
8 |
9 | /**
10 | * Method to execute when a new {@link MeasurementResult} is received.
11 | *
12 | * @param result The new result.
13 | */
14 | void onResult(MeasurementResult result);
15 |
16 | /**
17 | * Method to execute when a new {@link MeasurementError} is thrown.
18 | *
19 | * @param error The thrown error.
20 | */
21 | void onError(MeasurementError error);
22 | }
23 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MeasurementError.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | /**
4 | * Exception thrown when a measurement fails to execute
5 | */
6 | public class MeasurementError extends Exception {
7 | public final String taskName;
8 |
9 | public MeasurementError(String taskName, String reason) {
10 | super(reason);
11 | this.taskName = taskName;
12 | }
13 |
14 | public MeasurementError(String taskName, String reason, Throwable e) {
15 | super(reason, e);
16 | this.taskName = taskName;
17 | }
18 |
19 | public MeasurementError(String taskName, Exception e) {
20 | this(taskName, e.getMessage(), e);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/measures/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/antoine/Software/Android-SDK/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Android template
3 | # Built application files
4 | *.apk
5 | *.ap_
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 |
18 | # Gradle files
19 | .gradle/
20 | build/
21 |
22 | # Local configuration file (sdk path, etc)
23 | local.properties
24 |
25 | # Proguard folder generated by Eclipse
26 | proguard/
27 |
28 | # Log Files
29 | *.log
30 |
31 | # Android Studio Navigation editor temp files
32 | .navigation/
33 |
34 | # Android Studio captures folder
35 | captures/
36 |
37 | # Intellij
38 | *.iml
39 | .idea/
40 |
41 | # Keystore files
42 | *.jks
43 |
44 | # Mac files
45 | .DS_Store
46 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MeasurementExecutor.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | /**
4 | * Asynchronous task executing the given measurement tasks,
5 | * and calling the given {@link MeasurementCallback} for each returned success or error.
6 | */
7 | public class MeasurementExecutor implements Runnable {
8 | private final Measurement task;
9 | private final MeasurementCallback listener;
10 |
11 | public MeasurementExecutor(Measurement task, MeasurementCallback listener) {
12 | this.task = task;
13 | this.listener = listener;
14 | }
15 |
16 | @Override
17 | public void run() {
18 | try {
19 | listener.onResult(task.execute());
20 | } catch (MeasurementError error) {
21 | listener.onError(error);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/Rtt.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | /**
4 | * Definition of a ping RTT.
5 | */
6 | public class Rtt {
7 |
8 | public final float min;
9 |
10 | public final float avg;
11 |
12 | public final float max;
13 |
14 | public final float mdev;
15 |
16 | Rtt(float min, float avg, float max, float mdev) {
17 | this.min = min;
18 | this.avg = avg;
19 | this.max = max;
20 | this.mdev = mdev;
21 | }
22 |
23 | @Override
24 | public String toString() {
25 | return "Rtt{" +
26 | "min=" + min +
27 | ", avg=" + avg +
28 | ", max=" + max +
29 | ", mdev=" + mdev +
30 | "}";
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/ICMPConfig.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | /**
4 | * Configuration class for ICMP
5 | */
6 |
7 | public class ICMPConfig {
8 | /**
9 | * URL of the host to ping
10 | */
11 | private String url;
12 |
13 | /**
14 | * Time to live of the ICMP packet
15 | */
16 | private int ttl;
17 |
18 | public ICMPConfig(String url) {
19 | this.url = url;
20 | this.ttl = 42;
21 | }
22 |
23 | public String getUrl() {
24 | return url;
25 | }
26 |
27 | public void setUrl(String url) {
28 | this.url = url;
29 | }
30 |
31 | public int getTtl() {
32 | return ttl;
33 | }
34 |
35 | public void setTtl(int ttl) {
36 | this.ttl = ttl;
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return "ICMPConfig{" +
42 | "url='" + url + '\'' +
43 | ", ttl=" + ttl +
44 | '}';
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/TracerouteConfig.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | /**
4 | * Configuration class for ICMPTraceroute
5 | */
6 |
7 | public final class TracerouteConfig {
8 | /**
9 | * URL of the remote host
10 | */
11 | private final String url;
12 |
13 | /**
14 | * Maximum time to live of ICMP packets used in traceroute
15 | */
16 | private int ttlMax;
17 |
18 | public TracerouteConfig(String url) {
19 | this.url = url;
20 | this.ttlMax = 42;
21 | }
22 |
23 | public String getUrl() {
24 | return url;
25 | }
26 |
27 | public int getTtlMax() {
28 | return ttlMax;
29 | }
30 |
31 | public void setTtlMax(int ttlMax) {
32 | this.ttlMax = ttlMax;
33 | }
34 |
35 | @Override
36 | public String toString() {
37 | return "TracerouteConfig{" +
38 | "url='" + url + '\'' +
39 | ", ttlMax=" + ttlMax +
40 | '}';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/measures/src/main/java/org/xbill/DNS/PublicUDPClient.java:
--------------------------------------------------------------------------------
1 | package org.xbill.DNS;
2 |
3 | import java.io.IOException;
4 | import java.net.SocketAddress;
5 |
6 | /**
7 | * Wrap javadns UDPClient since it is only package visible.
8 | */
9 | public final class PublicUDPClient implements DNSClient {
10 | private final UDPClient client;
11 |
12 | public PublicUDPClient(long endTime) throws IOException {
13 | client = new UDPClient(endTime);
14 | }
15 |
16 | @Override
17 | public void bind(SocketAddress addr) throws IOException {
18 | client.bind(addr);
19 | }
20 |
21 | @Override
22 | public void connect(SocketAddress addr) throws IOException {
23 | client.connect(addr);
24 | }
25 |
26 | @Override
27 | public void send(byte[] data) throws IOException {
28 | client.send(data);
29 | }
30 |
31 | @Override
32 | public byte[] recv(int max) throws IOException {
33 | return client.recv(max);
34 | }
35 |
36 | @Override
37 | public void cleanup() throws IOException {
38 | client.cleanup();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/dns/DNSRecord.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.dns;
2 |
3 | import org.xbill.DNS.DClass;
4 | import org.xbill.DNS.Record;
5 | import org.xbill.DNS.Type;
6 |
7 | /**
8 | * Definition of a DNS record.
9 | */
10 | public class DNSRecord {
11 |
12 | /**
13 | * The recorded server name
14 | */
15 | public final String name;
16 |
17 | /**
18 | * The Record Type as String
19 | */
20 | public final String type;
21 |
22 | /**
23 | * The Record Class as String
24 | */
25 | public final String dclass;
26 |
27 | /**
28 | * The recorded Time To Live
29 | */
30 | public final long ttl;
31 |
32 | DNSRecord(Record record) {
33 | name = record.getName().toString();
34 | type = Type.string(record.getType());
35 | dclass = DClass.string(record.getDClass());
36 | ttl = record.getTTL();
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return "DNSRecord{" +
42 | "name='" + name + '\'' +
43 | ", type='" + type + '\'' +
44 | ", dclass='" + dclass + '\'' +
45 | ", ttl=" + ttl +
46 | '}';
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/Measurement.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | import android.os.AsyncTask;
4 |
5 | import java.util.concurrent.ExecutorService;
6 |
7 | /**
8 | * Common measurement behavior.
9 | */
10 | public abstract class Measurement {
11 | public final String taskName;
12 |
13 | protected Measurement(String taskName) {
14 | this.taskName = taskName;
15 | }
16 |
17 | /**
18 | * Ensure that the measurement is asynchronously called in an {@link ExecutorService}.
19 | *
20 | * @param callback The {@link MeasurementCallback} used
21 | * for reporting success or failure of this measurement.
22 | */
23 | public final void call(MeasurementCallback callback) {
24 | AsyncTask.execute(new MeasurementExecutor(this, callback));
25 | }
26 |
27 | /**
28 | * Actual, synchronous, process of a measurement.
29 | * This method has to be called from another thread than the UI one.
30 | *
31 | * @return The results of this measurement.
32 | * @throws MeasurementError If anything goes wrong during measurement.
33 | */
34 | public abstract MeasurementResult execute() throws MeasurementError;
35 | }
36 |
--------------------------------------------------------------------------------
/measures/src/main/java/org/xbill/DNS/PublicTCPClient.java:
--------------------------------------------------------------------------------
1 | package org.xbill.DNS;
2 |
3 | import java.io.IOException;
4 | import java.net.SocketAddress;
5 |
6 | /**
7 | * Wrap javadns TCPClient since it is only package visible.
8 | */
9 | public final class PublicTCPClient implements DNSClient {
10 | private final TCPClient client;
11 |
12 | public PublicTCPClient(long endTime) throws IOException {
13 | client = new TCPClient(endTime);
14 | }
15 |
16 | @Override
17 | public void bind(SocketAddress addr) throws IOException {
18 | client.bind(addr);
19 | }
20 |
21 | @Override
22 | public void connect(SocketAddress addr) throws IOException {
23 | client.connect(addr);
24 | }
25 |
26 | @Override
27 | public void send(byte[] data) throws IOException {
28 | client.send(data);
29 | }
30 |
31 | /**
32 | * Receive DNS data.
33 | *
34 | * @param max Not used in tcp implementation
35 | * @return The received content.
36 | * @throws IOException {@inheritDoc}
37 | */
38 | @Override
39 | public byte[] recv(int max) throws IOException {
40 | return client.recv();
41 | }
42 |
43 | @Override
44 | public void cleanup() throws IOException {
45 | client.cleanup();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/TracerouteResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | import io.apisense.network.MeasurementResult;
8 |
9 | /**
10 | * Class containing result of a {@link TracerouteTask}
11 | */
12 |
13 | public final class TracerouteResult extends MeasurementResult {
14 | /**
15 | * Configuration used for the traceroute.
16 | */
17 | private final TracerouteConfig config;
18 |
19 | /**
20 | * List of the hops of the Traceroute command
21 | */
22 | private final List hops;
23 |
24 | TracerouteResult(String taskName, long startTime, long endTime, TracerouteConfig config, ArrayList hops) {
25 | super(taskName, startTime, endTime);
26 | this.config = config;
27 | this.hops = Collections.unmodifiableList(hops);
28 |
29 | }
30 |
31 | public List getHops() {
32 | return Collections.unmodifiableList(hops);
33 | }
34 |
35 | public TracerouteConfig getConfig() {
36 | return config;
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return "TracerouteResult{" +
42 | "config=" + config +
43 | ", hops=" + hops +
44 | "} " + super.toString();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/nexusConfig.gradle:
--------------------------------------------------------------------------------
1 | // Default parameters
2 | if (!project.hasProperty("nexusUrl")) {
3 | ext.nexusUrl = "file:///tmp/sdkRepo"
4 | }
5 |
6 | if (!project.hasProperty("nexusUsername")) {
7 | ext.nexusUsername = ""
8 | }
9 |
10 | if (!project.hasProperty("nexusPassword")) {
11 | ext.nexusPassword = ""
12 | }
13 |
14 | ext.nexusRelease = "${nexusUrl}/service/local/staging/deploy/maven2/"
15 | ext.nexusSnapshot = "${nexusUrl}/content/repositories/snapshots/"
16 |
17 | private boolean signingConfigured() {
18 | return project.hasProperty("signing.keyId") && project.hasProperty("signing.password") \
19 | && project.hasProperty("signing.secretKeyRingFile")
20 | }
21 |
22 | allprojects {
23 | apply plugin: 'maven'
24 | if (signingConfigured()) {
25 | apply plugin: 'signing'
26 | signing {
27 | sign configurations.archives
28 | }
29 | }
30 |
31 | uploadArchives.repositories.mavenDeployer {
32 | if (signingConfigured()) {
33 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
34 | }
35 |
36 | repository(url: nexusRelease) {
37 | authentication(userName: nexusUsername, password: nexusPassword)
38 | }
39 |
40 | snapshotRepository(url: nexusSnapshot) {
41 | authentication(userName: nexusUsername, password: nexusPassword)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MeasurementResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 | /**
4 | * Abstract class containing the attributes shared by all the results returned the Measurement methods call()
5 | */
6 | public abstract class MeasurementResult {
7 | /**
8 | * Name of the task (ie TCP download, Traceroute, etc)
9 | */
10 | private final String taskName;
11 |
12 | /**
13 | * Time of the beginning of the task in milliseconds
14 | */
15 | private final long startTime;
16 |
17 | /**
18 | * Time of the end of the task in milliseconds
19 | */
20 | private final long endTime;
21 |
22 | /**
23 | * Number of milliseconds representing the duration of the task
24 | */
25 | private final long duration;
26 |
27 | protected MeasurementResult(String taskName, long startTime, long endTime) {
28 | this.taskName = taskName;
29 | this.startTime = startTime;
30 | this.endTime = endTime;
31 | this.duration = endTime - startTime;
32 | }
33 |
34 | public String getTaskName() {
35 | return taskName;
36 | }
37 |
38 | public long getStartTime() {
39 | return startTime;
40 | }
41 |
42 | public long getEndTime() {
43 | return endTime;
44 | }
45 |
46 | public long getDuration() {
47 | return duration;
48 | }
49 |
50 | @Override
51 | public String toString() {
52 | return "MeasurementResult{" +
53 | "taskName='" + taskName + '\'' +
54 | ", startTime=" + startTime +
55 | ", endTime=" + endTime +
56 | ", duration=" + duration +
57 | '}';
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/ICMPResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | import io.apisense.network.MeasurementResult;
4 |
5 | /**
6 | * Contains the output of a ping command
7 | */
8 | public final class ICMPResult extends MeasurementResult {
9 | /**
10 | * Configuration used for the burst.
11 | */
12 | private final ICMPConfig config;
13 |
14 | /**
15 | * Name of the host to ping
16 | */
17 | private final String hostname;
18 |
19 | /**
20 | * IP address of the host to ping
21 | */
22 | private final String ip;
23 |
24 | /**
25 | * Latency of the ping
26 | */
27 | private final long ping;
28 |
29 | /**
30 | * Time to live of the ICMP packet
31 | */
32 | private final int ttl;
33 |
34 | /**
35 | * Round-trip time of the ping
36 | */
37 | private final Rtt rtt;
38 |
39 | ICMPResult(long startTime, long endTime, ICMPConfig config, String hostname, String ip, long ping, int ttl, Rtt rtt) {
40 | super(ICMPTask.TAG, startTime, endTime);
41 | this.config = config;
42 | this.hostname = hostname;
43 | this.ip = ip;
44 | this.ping = ping;
45 | this.ttl = ttl;
46 | this.rtt = rtt;
47 | }
48 |
49 | public ICMPConfig getConfig() {
50 | return config;
51 | }
52 |
53 | public String getHostname() {
54 | return hostname;
55 | }
56 |
57 | public String getIp() {
58 | return ip;
59 | }
60 |
61 | public long getPing() {
62 | return ping;
63 | }
64 |
65 | public int getTtl() {
66 | return ttl;
67 | }
68 |
69 | public Rtt getRtt() {
70 | return rtt;
71 | }
72 |
73 | @Override
74 | public String toString() {
75 | return "ICMPResult{" +
76 | "config=" + config +
77 | ", hostname='" + hostname + '\'' +
78 | ", ip='" + ip + '\'' +
79 | ", ping=" + ping +
80 | ", ttl=" + ttl +
81 | ", rtt=" + rtt +
82 | "} " + super.toString();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://search.maven.org/#artifactdetails%7Cio.apisense.network%7Candroid-network-measures%7C1.1.2%7Caar)
2 |
3 | # Android Network measures
4 |
5 | We aim to provide a minimalistic library to perform network measures on Android.
6 |
7 |
8 | ## Available tests
9 |
10 | Currently there are 7 available test types:
11 |
12 | - DNS lookup
13 | - Ping
14 | - Traceroute
15 | - TCP Download
16 | - TCP Upload
17 | - UDP Download
18 | - UDP Upload
19 |
20 | Thoses tests are for the most heavily inspired from [Mobilyzer](https://github.com/mobilyzer/Mobilyzer),
21 | but easier to use (at least we hope).
22 |
23 | ## Usage example
24 |
25 | ### Require dependency
26 |
27 | #### Maven
28 |
29 |
30 | io.apisense.network
31 | android-network-measures
32 | 1.1.0
33 |
34 |
35 | #### Gradle
36 |
37 | compile 'io.apisense.network:android-network-measures:1.1.0'
38 |
39 | ### Call a measurement
40 |
41 | Here is an example of a DNS test:
42 |
43 | import io.apisense.network.dns.DNSLookupConfig;
44 | import io.apisense.network.dns.DNSLookupTask;
45 | import io.apisense.network.MeasurementCallback;
46 | import io.apisense.network.MeasurementResult;
47 | import io.apisense.network.MeasurementError;
48 |
49 | DNSLookupConfig config = new DNSLookupConfig("www.google.com"); // Mandatory configurations
50 | config.setServer("8.8.8.8"); // Every optional configurations are accessible via setters
51 |
52 | DNSLookupTask dnsLookup = new DNSLookupTask(config);
53 | dnsLookup.call(new MeasurementCallback() { // Measurement is processed in an AsyncTask
54 | // Callback is executed on UI thread
55 | public void onResult(MeasurementResult result) {
56 | // ...
57 | }
58 |
59 | public void onError(MeasurementError error) {
60 | // ...
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPBurstResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import io.apisense.network.MeasurementResult;
4 |
5 | /**
6 | * Class containing result of {@link UDPBurstTask} (upload and download)
7 | */
8 |
9 | public final class UDPBurstResult extends MeasurementResult {
10 | /**
11 | * Configuration used for the burst.
12 | */
13 | private final UDPBurstConfig config;
14 |
15 | /**
16 | * Number of packets sent during the test
17 | */
18 | private final int packetCount;
19 |
20 | /**
21 | * ratio (between 0 and 1) of packets out of order.
22 | *
23 | * Out-of-order packets are defined as arriving packets
24 | * with sequence numbers smaller than their predecessors
25 | */
26 | private final double outOfOrderRatio;
27 |
28 | /**
29 | * Jitter as specified in RFC3393
30 | */
31 | private final long jitter;
32 |
33 | /**
34 | * Number of packet lost during burst.
35 | */
36 | private final int lostCount;
37 |
38 | public UDPBurstResult(String taskName, long startTime, long endTime, UDPBurstConfig config, int packetCount, int lostCount, double outOfOrderRatio, long jitter) {
39 | super(taskName, startTime, endTime);
40 | this.config = config;
41 | this.packetCount = packetCount;
42 | this.lostCount = lostCount;
43 | this.outOfOrderRatio = outOfOrderRatio;
44 | this.jitter = jitter;
45 | }
46 |
47 | public UDPBurstConfig getConfig() {
48 | return config;
49 | }
50 |
51 | public int getPacketCount() {
52 | return packetCount;
53 | }
54 |
55 | public int getLostCount() {
56 | return lostCount;
57 | }
58 |
59 | public double getOutOfOrderRatio() {
60 | return outOfOrderRatio;
61 | }
62 |
63 | public long getJitter() {
64 | return jitter;
65 | }
66 |
67 | @Override
68 | public String toString() {
69 | return "UDPBurstResult{" +
70 | "config=" + config +
71 | ", packetCount=" + packetCount +
72 | ", outOfOrderRatio=" + outOfOrderRatio +
73 | ", jitter=" + jitter +
74 | ", lostCount=" + lostCount +
75 | "} " + super.toString();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/dns/DNSLookupResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.dns;
2 |
3 | import org.xbill.DNS.Flags;
4 | import org.xbill.DNS.Message;
5 | import org.xbill.DNS.Rcode;
6 | import org.xbill.DNS.Record;
7 |
8 | import java.util.ArrayList;
9 | import java.util.Arrays;
10 | import java.util.List;
11 |
12 | import io.apisense.network.MeasurementResult;
13 |
14 | /**
15 | * Class containing result of {@link DNSLookupTask}
16 | */
17 | public class DNSLookupResult extends MeasurementResult {
18 |
19 | /**
20 | * DNS response code for the query.
21 | */
22 | private final String responseCode;
23 | /**
24 | * Is the response truncated?
25 | */
26 | private final boolean truncated;
27 | /**
28 | * Report configuration used on this query.
29 | */
30 | private DNSLookupConfig configuration;
31 | /**
32 | * List of the actual DNS records for the queried domain.
33 | */
34 | private List records;
35 |
36 |
37 | public DNSLookupResult(DNSLookupConfig configuration, long startTime, long endTime,
38 | String rCode, boolean tc, List records) {
39 | super(DNSLookupTask.TAG, startTime, endTime);
40 | this.configuration = configuration;
41 | this.responseCode = rCode;
42 | this.truncated = tc;
43 | this.records = new ArrayList<>();
44 | for (Record record : records) {
45 | this.records.add(new DNSRecord(record));
46 | }
47 | }
48 |
49 | static DNSLookupResult fromMessage(DNSLookupConfig config, Message response, long startTime, long endTime) {
50 | return new DNSLookupResult(config,
51 | startTime, endTime,
52 | Rcode.string(response.getHeader().getRcode()),
53 | response.getHeader().getFlag(Flags.TC),
54 | Arrays.asList(response.getSectionArray(1))
55 | );
56 | }
57 |
58 | public DNSLookupConfig getConfiguration() {
59 | return configuration;
60 | }
61 |
62 | public String getResponseCode() {
63 | return responseCode;
64 | }
65 |
66 | public boolean isTruncated() {
67 | return truncated;
68 | }
69 |
70 | public List getRecords() {
71 | return records;
72 | }
73 |
74 | @Override
75 | public String toString() {
76 | return "DNSLookupResult{" +
77 | "configuration=" + configuration +
78 | ", responseCode='" + responseCode + '\'' +
79 | ", truncated=" + truncated +
80 | ", records=" + records +
81 | '}';
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/tcp/TCPThroughputResult.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.tcp;
2 |
3 | import java.util.Collections;
4 | import java.util.List;
5 |
6 | import io.apisense.network.MeasurementResult;
7 |
8 | /**
9 | * Class containing result of {@link TCPThroughputTask} (upload and download)
10 | */
11 | public final class TCPThroughputResult extends MeasurementResult {
12 | /**
13 | * Configuration used for the throughput test.
14 | */
15 | private final TCPThroughputConfig config;
16 |
17 | /**
18 | * Contains the number of bytes received or sent depending of the kind of test,
19 | * for each sample period, ordered in ascending order
20 | */
21 | private final List tcpSpeedResults;
22 |
23 | /**
24 | * Median throughput of all tests in bytes.
25 | */
26 | private final double medianThroughput;
27 |
28 | /**
29 | * Data used (sent or received) after slow start period in bits
30 | */
31 | private final long usedData;
32 |
33 | public TCPThroughputResult(String taskName, long startTime, long endTime, TCPThroughputConfig config, List tcpSpeedResults, long dataConsumedAfterSlowStart) {
34 | super(taskName, startTime, endTime);
35 | this.config = config;
36 | this.usedData = dataConsumedAfterSlowStart;
37 | this.tcpSpeedResults = Collections.unmodifiableList(tcpSpeedResults);
38 | this.medianThroughput = tcpSpeedResults.isEmpty() ? 0 : computeMedianSpeedPerSecond();
39 | }
40 |
41 | public TCPThroughputConfig getConfig() {
42 | return config;
43 | }
44 |
45 | public List getTcpSpeedResults() {
46 | return Collections.unmodifiableList(tcpSpeedResults);
47 | }
48 |
49 | public double getMedianThroughput() {
50 | return medianThroughput;
51 | }
52 |
53 | public long getUsedData() {
54 | return usedData;
55 | }
56 |
57 | private double computeMedianSpeedPerSecond() {
58 | double result;
59 | if (tcpSpeedResults.size() % 2 == 0) {
60 | result = tcpSpeedResults.get(tcpSpeedResults.size() / 2) + tcpSpeedResults.get(tcpSpeedResults.size() / 2 - 1) / 2;
61 | } else {
62 | result = tcpSpeedResults.get((tcpSpeedResults.size() - 1) / 2);
63 | }
64 | return result;
65 | }
66 |
67 | public String toString() {
68 | String s = super.toString() + "\n";
69 | s += "TCP Speed Results : [";
70 | for (Double d : tcpSpeedResults) {
71 | s += String.valueOf(d) + " ";
72 | }
73 | s += "]";
74 | return s;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPBurstTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import android.support.annotation.NonNull;
4 |
5 | import java.io.IOException;
6 | import java.net.DatagramPacket;
7 | import java.net.DatagramSocket;
8 | import java.net.SocketException;
9 |
10 | import io.apisense.network.Measurement;
11 | import io.apisense.network.MeasurementError;
12 |
13 | /**
14 | * Abstract class containing common code used for UDP upload and download tests
15 | */
16 | public abstract class UDPBurstTask extends Measurement {
17 | protected static final int DEFAULT_PORT = 31341;
18 |
19 | /**
20 | * round-trip delay, in msec.
21 | */
22 | protected static final int RCV_TIMEOUT = 2000;
23 |
24 | protected long startTimeTask; //time in milliseconds
25 | protected long endTimeTask; //time in milliseconds
26 |
27 | protected UDPBurstConfig config;
28 |
29 |
30 | public UDPBurstTask(String taskName, UDPBurstConfig udpBurstConfig) {
31 | super(taskName);
32 | this.config = udpBurstConfig;
33 | }
34 |
35 | /**
36 | * Wait for the socket to retrieve a response to the previous burst.
37 | *
38 | * @param sock The socket to listen through.
39 | * @return An {@link UDPPacket} containing the response.
40 | * @throws MeasurementError If any error occurred during measurement.
41 | */
42 | @NonNull
43 | protected UDPPacket retrieveResponseDatagram(DatagramSocket sock) throws MeasurementError {
44 | byte[] buffer = new byte[config.getPacketSizeByte()];
45 | DatagramPacket recvpacket = new DatagramPacket(buffer, buffer.length);
46 |
47 | try {
48 | sock.receive(recvpacket);
49 | } catch (SocketException e1) {
50 | throw new MeasurementError(taskName, "Timed out reading from " + config.getTargetIp(), e1);
51 | } catch (IOException e) {
52 | throw new MeasurementError(taskName, "Error reading from " + config.getTargetIp(), e);
53 | }
54 |
55 | return new UDPPacket(taskName, recvpacket.getData());
56 | }
57 |
58 | /**
59 | * Opens a datagram (UDP) socket
60 | *
61 | * @return a datagram socket used for sending/receiving
62 | * @throws MeasurementError if an error occurs
63 | */
64 | protected DatagramSocket openSocket() throws MeasurementError {
65 | DatagramSocket sock;
66 |
67 | // Open datagram socket
68 | try {
69 | sock = new DatagramSocket();
70 | sock.setSoTimeout(RCV_TIMEOUT);
71 | } catch (SocketException e) {
72 | throw new MeasurementError(taskName, "Socket creation failed", e);
73 | }
74 |
75 | return sock;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/tcp/TCPDownloadTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.tcp;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.net.Socket;
8 |
9 | import io.apisense.network.MeasurementError;
10 | import io.apisense.network.MeasurementResult;
11 |
12 |
13 | /**
14 | * Measurement class used to realise an TCP download test
15 | */
16 | public class TCPDownloadTask extends TCPThroughputTask {
17 | private static final String TAG = "TCPDownloadTask";
18 | private static final int PORT_DOWNLINK = 6001;
19 |
20 | public TCPDownloadTask(TCPThroughputConfig tcpThroughputConfig) {
21 | super(TAG, tcpThroughputConfig);
22 | }
23 |
24 | /**
25 | * {@inheritDoc}
26 | *
27 | * @return A {@link TCPThroughputResult} object containing information on the TCP download test.
28 | * @throws MeasurementError {@inheritDoc}
29 | */
30 | public MeasurementResult execute() throws MeasurementError {
31 | Log.d(TAG, "Start");
32 | Socket tcpSocket = buildUpSocket(config.getTarget(), PORT_DOWNLINK);
33 |
34 | try {
35 | this.taskStartTime = System.currentTimeMillis();
36 | retrieveData(tcpSocket);
37 | this.taskEndTime = System.currentTimeMillis();
38 | } catch (OutOfMemoryError e) { // TODO: See if this catch clause is really necessary
39 | throw new MeasurementError(taskName, "Detect out of memory at Downlink task.", e);
40 | } finally {
41 | closeSocket(tcpSocket);
42 | }
43 | Log.d(TAG, "Finished");
44 |
45 | return new TCPThroughputResult(TAG, this.taskStartTime, this.taskEndTime,
46 | config, this.samplingResults, this.dataConsumedAfterSlowStart);
47 | }
48 |
49 | /**
50 | * Read the data sent by the server to the socket,
51 | * and update the performance statistics accordingly.
52 | *
53 | * @param tcpSocket The socket to read through
54 | * @throws MeasurementError If the socket interaction fails.
55 | */
56 | private void retrieveData(Socket tcpSocket) throws MeasurementError {
57 | int read_bytes;
58 | byte[] buffer = new byte[BUFFER_SIZE];
59 | InputStream iStream = null;
60 | try {
61 | iStream = tcpSocket.getInputStream();
62 | do {
63 | read_bytes = iStream.read(buffer, 0, buffer.length);
64 | updateSize(read_bytes);
65 | } while (read_bytes >= 0);
66 | } catch (IOException e) {
67 | throw new MeasurementError(taskName, "Error to receive data from " + config.getTarget(), e);
68 | } finally {
69 | closeStream(iStream);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/tcp/TCPThroughputConfig.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.tcp;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 |
6 | /**
7 | * Configuration class for both upload an download TCPThroughputTask
8 | */
9 | public class TCPThroughputConfig {
10 | /**
11 | * IP address of the remote server used for the test
12 | */
13 | private InetAddress target;
14 |
15 | /**
16 | * URL of the remote server used for the test
17 | */
18 | private String url;
19 |
20 | /**
21 | * Data limit for upload in Mb
22 | */
23 | private int dataLimitMbUp = 7;
24 |
25 | /**
26 | * Size of a single packet in bytes
27 | */
28 | private int pktSizeUpBytes = 700;
29 |
30 | /**
31 | * Duration of a sample period in second
32 | */
33 | private float samplePeriodSec = 0.5f;
34 |
35 | /**
36 | * Duration of the slow start period in float
37 | */
38 | private float slowStartPeriodSec = 0.5f;
39 |
40 | /**
41 | * Timeout of the TCP connection established for the test
42 | */
43 | private int tcpTimeoutSec = 15;
44 |
45 | public TCPThroughputConfig(String url) throws UnknownHostException {
46 | setUrl(url);
47 | }
48 |
49 | public String getUrl() {
50 | return url;
51 | }
52 |
53 | public void setUrl(String url) throws UnknownHostException {
54 | this.url = url;
55 | target = InetAddress.getByName(url);
56 | }
57 |
58 | public InetAddress getTarget() {
59 | return target;
60 | }
61 |
62 | public int getDataLimitMbUp() {
63 | return dataLimitMbUp;
64 | }
65 |
66 | public void setDataLimitMbUp(int dataLimitMbUp) {
67 | this.dataLimitMbUp = dataLimitMbUp;
68 | }
69 |
70 | public int getPktSizeUpBytes() {
71 | return pktSizeUpBytes;
72 | }
73 |
74 | public void setPktSizeUpBytes(int pktSizeUpBytes) {
75 | this.pktSizeUpBytes = pktSizeUpBytes;
76 | }
77 |
78 | public float getSamplePeriodSec() {
79 | return samplePeriodSec;
80 | }
81 |
82 | public void setSamplePeriodSec(float samplePeriodSec) {
83 | this.samplePeriodSec = samplePeriodSec;
84 | }
85 |
86 | public float getSlowStartPeriodSec() {
87 | return slowStartPeriodSec;
88 | }
89 |
90 | public void setSlowStartPeriodSec(float slowStartPeriodSec) {
91 | this.slowStartPeriodSec = slowStartPeriodSec;
92 | }
93 |
94 | public int getTcpTimeoutSec() {
95 | return tcpTimeoutSec;
96 | }
97 |
98 | public void setTcpTimeoutSec(int tcpTimeoutSec) {
99 | this.tcpTimeoutSec = tcpTimeoutSec;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/TracerouteTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 |
4 | import android.util.Log;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | import io.apisense.network.Measurement;
10 | import io.apisense.network.MeasurementError;
11 | import io.apisense.network.MeasurementResult;
12 |
13 | /**
14 | * Measurement class used to realise a Traceroute
15 | */
16 | public class TracerouteTask extends Measurement {
17 | private static final String TAG = "ICMPTraceroute";
18 | private final TracerouteConfig config;
19 | private String destIp;
20 | private ICMPConfig icmpConfig;
21 |
22 | public TracerouteTask(TracerouteConfig tracerouteConfig) {
23 | super(TAG);
24 | this.config = tracerouteConfig;
25 | this.icmpConfig = new ICMPConfig(config.getUrl());
26 | }
27 |
28 | @Override
29 | public MeasurementResult execute() throws MeasurementError {
30 | try {
31 | destIp = new ICMPTask(icmpConfig).execute().getIp();
32 | } catch (MeasurementError error) {
33 | throw new MeasurementError(taskName, "Could not determine destination IP", error);
34 | }
35 |
36 | long taskStartTime = System.currentTimeMillis();
37 | ArrayList traces = new ArrayList<>();
38 | traceroute(1, traces);
39 | long taskEndTime = System.currentTimeMillis();
40 | return new TracerouteResult(TAG, taskStartTime, taskEndTime, config, traces);
41 | }
42 |
43 | private void traceroute(int currentTtl, List traces) {
44 | ICMPResult hop = null;
45 | try {
46 | icmpConfig.setTtl(currentTtl);
47 | hop = new ICMPTask(icmpConfig).execute();
48 | Log.v(TAG, "A new ICMPResult : " + hop);
49 | traces.add(hop);
50 | } catch (MeasurementError e) {
51 | Log.w(TAG, "Error on ICMPResult (dst: " + config.getUrl() + ", ttl: " + currentTtl + ")", e);
52 | } finally {
53 | if (notThereYet(hop, currentTtl)) {
54 | traceroute(currentTtl + 1, traces);
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Defines if the traceroute should continue, by checking that:
61 | * - The current TTL is under the TTL limit.
62 | * - The current node is NOT the target node.
63 | *
64 | * @param hop The current hop.
65 | * @param currentTtl The TTL used for this ping.
66 | * @return True if the traceroute should iterate at least one more time, false otherwise.
67 | */
68 | private boolean notThereYet(ICMPResult hop, int currentTtl) {
69 | if (hop == null) { // If a node doesn't answer, we try the next one.
70 | return currentTtl < config.getTtlMax();
71 | }
72 | return currentTtl < config.getTtlMax() && !destIp.equals(hop.getIp());
73 | }
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/MetricCalculator.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import java.util.ArrayList;
4 |
5 | /**
6 | * @author Hongyi Yao (hyyao@umich.edu) This class calculates the out-of-order ratio and delay
7 | * jitter in the array of received UDP packets
8 | */
9 | final class MetricCalculator {
10 | private int maxPacketNum;
11 | private ArrayList offsetedDelayList;
12 | private int packetCount;
13 | private int outOfOrderCount;
14 |
15 | public MetricCalculator() {
16 | maxPacketNum = -1;
17 | offsetedDelayList = new ArrayList<>();
18 | packetCount = 0;
19 | outOfOrderCount = 0;
20 | }
21 |
22 | /**
23 | * Out-of-order packets is defined as arriving packets with sequence numbers smaller than their
24 | * predecessors.
25 | *
26 | * @param packetNum: packet number in burst sequence
27 | * @param timestamp: estimated one-way delay(contains clock offset)
28 | */
29 | public void addPacket(int packetNum, long timestamp) {
30 | if (packetNum > maxPacketNum) {
31 | maxPacketNum = packetNum;
32 | } else {
33 | outOfOrderCount++;
34 | }
35 | offsetedDelayList.add(System.currentTimeMillis() - timestamp);
36 | packetCount++;
37 | }
38 |
39 | /**
40 | * Out-of-order ratio is defined as the ratio between the number of out-of-order packets and the
41 | * total number of packets.
42 | *
43 | * @return the inversion number of the current UDP burst
44 | */
45 | public double calculateOutOfOrderRatio() {
46 | if (packetCount != 0) {
47 | return (double) outOfOrderCount / packetCount;
48 | } else {
49 | return 0.0;
50 | }
51 | }
52 |
53 | /**
54 | * Calculate jitter as the standard deviation of one-way delays[RFC3393] We can assume the clock
55 | * offset between server and client is constant in a short period(several milliseconds) since
56 | * typical oscillators have no more than 100ppm of frequency error , then it will be cancelled
57 | * out during the calculation process
58 | *
59 | * @return the jitter of UDP burst
60 | */
61 | public long calculateJitter() {
62 | if (packetCount > 1) {
63 | double offsetedDelay_mean = 0;
64 | for (long offsetedDelay : offsetedDelayList) {
65 | offsetedDelay_mean += (double) offsetedDelay / packetCount;
66 | }
67 |
68 | double jitter = 0;
69 | for (long offsetedDelay : offsetedDelayList) {
70 | jitter += ((double) offsetedDelay - offsetedDelay_mean)
71 | * ((double) offsetedDelay - offsetedDelay_mean) / (packetCount - 1);
72 | }
73 | jitter = Math.sqrt(jitter);
74 |
75 | return (long) jitter;
76 | } else {
77 | return 0;
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPDownloadBurstTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 |
4 | import android.util.Log;
5 |
6 | import java.io.IOException;
7 | import java.net.DatagramSocket;
8 |
9 | import io.apisense.network.MeasurementError;
10 | import io.apisense.network.MeasurementResult;
11 |
12 | /**
13 | * Measurement class used to realise an UDP download burst
14 | * Measures the jitter, the jitter, the loss, and the number of out of order packets in download
15 | */
16 | public class UDPDownloadBurstTask extends UDPBurstTask {
17 | public static final String TAG = "UDPDownloadBurst";
18 |
19 | public UDPDownloadBurstTask(UDPBurstConfig udpBurstConfig) {
20 | super(TAG, udpBurstConfig);
21 | }
22 |
23 | /**
24 | * {@inheritDoc}
25 | *
26 | * @return A {@link UDPBurstResult} object containing information on the UDP download burst.
27 | * @throws MeasurementError {@inheritDoc}
28 | */
29 | public MeasurementResult execute() throws MeasurementError {
30 | MetricCalculator metricCalculator = new MetricCalculator();
31 | DatagramSocket sock = openSocket();
32 |
33 | startTimeTask = System.currentTimeMillis();
34 | UDPPacket dataPacket;
35 | int pktRecv = 0;
36 |
37 | sendDownloadRequest(sock);
38 |
39 | for (int i = 0; i < config.getUdpBurstCount(); i++) {
40 | dataPacket = retrieveResponseDatagram(sock);
41 |
42 | if (dataPacket.type == UDPPacket.PKT_DATA) {
43 | Log.v(TAG, "Received packed n°" + dataPacket.packetNum);
44 | pktRecv++;
45 | metricCalculator.addPacket(dataPacket.packetNum, dataPacket.timestamp);
46 | } else {
47 | throw new MeasurementError(taskName, "Error closing input stream from " + config.getTargetIp());
48 | }
49 | }
50 | endTimeTask = System.currentTimeMillis();
51 | sock.close();
52 |
53 | double outOfOrderRatio = metricCalculator.calculateOutOfOrderRatio();
54 | long jitter = metricCalculator.calculateJitter();
55 | int lostCount = config.getUdpBurstCount() - pktRecv;
56 | return new UDPBurstResult(TAG, this.startTimeTask, this.endTimeTask, config,
57 | pktRecv, lostCount, outOfOrderRatio, jitter);
58 | }
59 |
60 | /**
61 | * Send a request packet to download with UDP.
62 | *
63 | * @param sock The socket to send packets through.
64 | * @throws MeasurementError If any error occurred during measurement.
65 | */
66 | private void sendDownloadRequest(DatagramSocket sock) throws MeasurementError {
67 | UDPPacket requestPacket = new UDPPacket(taskName, UDPPacket.PKT_REQUEST, this.config);
68 | try {
69 | sock.send(requestPacket.createDatagram(config.getTargetIp(), DEFAULT_PORT));
70 | } catch (IOException e) {
71 | throw new MeasurementError(taskName, "Error while sending download burst request on " + config.getTargetIp(), e);
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/measures/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "25.0.0"
6 | defaultConfig {
7 | minSdkVersion 15
8 | targetSdkVersion 25
9 | versionCode computeVersionCode()
10 | versionName version
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 | }
19 |
20 | private int computeVersionCode() {
21 | def (major, minor, fix) = version.tokenize(".")
22 | fix = fix.tokenize("-")[0] // Remove snapshot if present
23 | return (major as Integer) * 100 + (minor as Integer) * 10 + (fix as Integer)
24 | }
25 |
26 | dependencies {
27 | compile 'com.android.support:appcompat-v7:25.1.0'
28 |
29 | compile 'dnsjava:dnsjava:2.1.7'
30 | compile 'com.google.code.gson:gson:2.8.0'
31 |
32 | testCompile 'junit:junit:4.12'
33 | }
34 |
35 | task("javadoc", type: Javadoc) {
36 | title = 'javadoc'
37 | description = 'Generate the project documentation'
38 | source = android.sourceSets.main.java.srcDirs
39 | classpath = project.files(android.getBootClasspath().join(File.pathSeparator))
40 | options {
41 | links "http://docs.oracle.com/javase/7/docs/api/"
42 | linksOffline "http://d.android.com/reference", "${android.sdkDirectory}/docs/reference"
43 | }
44 | exclude '**/BuildConfig.java'
45 | exclude '**/R.java'
46 | failOnError false
47 | }
48 |
49 | afterEvaluate {
50 | javadoc.classpath += files(android.libraryVariants.collect { variant ->
51 | variant.javaCompile.classpath.files
52 | })
53 | }
54 |
55 | task javadocJar(type: Jar, dependsOn: javadoc) {
56 | classifier = 'javadoc'
57 | from javadoc.destinationDir
58 | }
59 |
60 | task sourcesJar(type: Jar) {
61 | classifier = 'sources'
62 | from android.sourceSets.main.java.srcDirs
63 | }
64 |
65 | artifacts {
66 | archives javadocJar, sourcesJar
67 | }
68 |
69 | uploadArchives {
70 | repositories.mavenDeployer {
71 | pom.project {
72 | name 'Android Network Measures'
73 | artifactId 'android-network-measures'
74 | packaging 'aar'
75 | description 'Provide network QoS & QoE measurement tools for Android.'
76 |
77 | url siteUrl
78 |
79 | scm {
80 | connection 'scm:git:https://github.com/APISENSE/android-network-measures.git'
81 | developerConnection 'scm:git:https://github.com/APISENSE/android-network-measures'
82 | url 'https://github.com/APISENSE/android-network-measures'
83 | }
84 |
85 | licenses {
86 | license commonLicense
87 | }
88 | developers {
89 | devs.collect({ developer it })
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPBurstConfig.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 |
6 | import io.apisense.network.MeasurementConfigException;
7 |
8 | /**
9 | * Configuration class for both upload an download UDPBurstTask
10 | */
11 |
12 | public class UDPBurstConfig {
13 | /**
14 | * Min packet size = (int type) + (int burstCount) + (int packetNum) + (int intervalNum) + (long
15 | * timestamp) + (int packetSize) + (int seq) + (int udpInterval) = 36
16 | */
17 | private static final int MIN_PACKETSIZE = 36;
18 |
19 | /**
20 | * Leave enough margin for min MTU in the link and IP options.
21 | */
22 | private static final int MAX_PACKETSIZE = 500;
23 |
24 | /**
25 | * Size of the packets in bytes
26 | */
27 | private int packetSizeByte = 100;
28 |
29 | /**
30 | * Number of packet to send by burst.
31 | */
32 | private int udpBurstCount = 16;
33 |
34 | /**
35 | * Interval between burst (µs).
36 | *
37 | * This value will be rounded in {@link UDPPacket#UDPPacket(int, UDPBurstConfig)}.
38 | */
39 | private int udpInterval = 500;
40 |
41 | /**
42 | * IP address of the remote server used for the test
43 | */
44 | private InetAddress targetIp;
45 |
46 | /**
47 | * URL of the remote server used for the test
48 | */
49 | private String url;
50 |
51 | public UDPBurstConfig(String url, int packetSizeByte) throws MeasurementConfigException {
52 | setUrl(url);
53 | if (packetSizeByte >= UDPBurstConfig.MIN_PACKETSIZE
54 | && packetSizeByte <= UDPBurstConfig.MAX_PACKETSIZE) {
55 | this.packetSizeByte = packetSizeByte;
56 | } else {
57 | throw new MeasurementConfigException("PacketSizeByte must be between "
58 | + String.valueOf(UDPBurstConfig.MIN_PACKETSIZE) + " and "
59 | + String.valueOf(UDPBurstConfig.MAX_PACKETSIZE));
60 | }
61 | }
62 |
63 | public int getPacketSizeByte() {
64 | return packetSizeByte;
65 | }
66 |
67 | public void setPacketSizeByte(int packetSizeByte) {
68 | this.packetSizeByte = packetSizeByte;
69 | }
70 |
71 | public int getUdpBurstCount() {
72 | return udpBurstCount;
73 | }
74 |
75 | public void setUdpBurstCount(int udpBurstCount) {
76 | this.udpBurstCount = udpBurstCount;
77 | }
78 |
79 | public int getUdpInterval() {
80 | return udpInterval;
81 | }
82 |
83 | public void setUdpInterval(int udpInterval) {
84 | this.udpInterval = udpInterval;
85 | }
86 |
87 | public InetAddress getTargetIp() {
88 | return targetIp;
89 | }
90 |
91 | public String getUrl() {
92 | return url;
93 | }
94 |
95 | public void setUrl(String url) throws MeasurementConfigException {
96 | this.url = url;
97 | try {
98 | targetIp = InetAddress.getByName(url);
99 | } catch (UnknownHostException e) {
100 | throw new MeasurementConfigException(e);
101 | }
102 | }
103 |
104 | @Override
105 | public String toString() {
106 | return "UDPBurstConfig{" +
107 | "packetSizeByte=" + packetSizeByte +
108 | ", udpBurstCount=" + udpBurstCount +
109 | ", udpInterval=" + udpInterval +
110 | ", targetIp=" + targetIp +
111 | ", url='" + url + '\'' +
112 | '}';
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPUploadBurstTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.util.Log;
5 |
6 | import java.io.IOException;
7 | import java.net.DatagramSocket;
8 |
9 | import io.apisense.network.MeasurementError;
10 | import io.apisense.network.MeasurementResult;
11 |
12 | /**
13 | * Measurement class used to realise an UDP upload burst
14 | * Measures the jitter, the jitter, the loss, and the number of out of order packets in upload
15 | */
16 | public class UDPUploadBurstTask extends UDPBurstTask {
17 | private static final String TAG = "UDPUploadBurst";
18 |
19 | public UDPUploadBurstTask(UDPBurstConfig udpBurstConfig) {
20 | super(TAG, udpBurstConfig);
21 | }
22 |
23 | /**
24 | * {@inheritDoc}
25 | *
26 | * @return A {@link UDPBurstResult} object containing information on the UDP upload burst.
27 | * @throws MeasurementError {@inheritDoc}
28 | */
29 | public MeasurementResult execute() throws MeasurementError {
30 | DatagramSocket sock = openSocket();
31 |
32 | UDPPacket dataPacket;
33 | startTimeTask = System.currentTimeMillis();
34 |
35 | // Send burst
36 | for (int i = 0; i < config.getUdpBurstCount(); i++) {
37 | dataPacket = new UDPPacket(taskName, UDPPacket.PKT_DATA, this.config);
38 | dataPacket.packetNum = i;
39 |
40 | // Flatten UDP packet
41 | try {
42 | sock.send(dataPacket.createDatagram(config.getTargetIp(), DEFAULT_PORT));
43 | } catch (IOException e) {
44 | sock.close();
45 | throw new MeasurementError(taskName, "Error while sending upload burst on " + config.getTargetIp(), e);
46 | }
47 |
48 | // Sleep udpInterval millisecond
49 | try {
50 | int timeMs = config.getUdpInterval() / 1000;
51 | int timeµs = config.getUdpInterval() % 1000;
52 | Thread.sleep(timeMs, timeµs * 1000);
53 | } catch (InterruptedException e) {
54 | Log.w(TAG, "Wait interrupted on UDP burst", e);
55 | }
56 | }
57 | endTimeTask = System.currentTimeMillis();
58 |
59 | // Receive response
60 | try {
61 | UDPPacket responsePacket = retrieveResponseDatagram(sock);
62 | return buildResult(responsePacket);
63 | } catch (MeasurementError error) {
64 | sock.close();
65 | throw error;
66 | }
67 | }
68 |
69 | /**
70 | * Analyse a response packet and create an {@link UDPBurstResult} from its content.
71 | *
72 | * @param responsePacket The packet to build results from.
73 | * @return The {@link UDPBurstResult} built from the response.
74 | * @throws MeasurementError If the given packet is not a response packet.
75 | */
76 | @NonNull
77 | private UDPBurstResult buildResult(UDPPacket responsePacket) throws MeasurementError {
78 | // Reconstruct UDP packet from flattened network data
79 | if (responsePacket.type == UDPPacket.PKT_RESPONSE) {
80 | int packetCount = responsePacket.packetNum;
81 | double outOfOrderRatio = (double) responsePacket.outOfOrderNum / responsePacket.packetNum;
82 | long jitter = responsePacket.timestamp;
83 | int lostCount = config.getUdpBurstCount() - packetCount;
84 | return new UDPBurstResult(TAG, this.startTimeTask, this.endTimeTask, config,
85 | packetCount, lostCount, outOfOrderRatio, jitter);
86 | } else {
87 | throw new MeasurementError(taskName, "Error: not a response packet! seq: " + responsePacket.seq);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/dns/DNSLookupConfig.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.dns;
2 |
3 | import android.util.Log;
4 |
5 | import java.lang.reflect.Method;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * The description of DNS lookup measurement
11 | */
12 | public class DNSLookupConfig {
13 | private static final String TAG = "DNSLookupConfig";
14 | /**
15 | * Domain name or IP to query.
16 | */
17 | private final String target;
18 |
19 | /**
20 | * DNS server to use for the query,
21 | * Will retrieve the configured ones from the phone by default.
22 | */
23 | private String server = retrieveDeviceServers()[0];
24 |
25 | /**
26 | * Query class,
27 | * by default to 'IN'.
28 | */
29 | private String qclass = "IN";
30 |
31 | /**
32 | * Query type, is determined dependending on the target.
33 | * By default to 'A'.
34 | */
35 | private String qtype = "A";
36 |
37 | /**
38 | * Explicitly ask to the DNS request to be over TCP.
39 | */
40 | private boolean forceTCP = false;
41 |
42 | /**
43 | * Timeout of the DNS request, in milliseconds.
44 | * default value: 5000 ms.
45 | */
46 | private int timeout = 5000;
47 |
48 | public DNSLookupConfig(String target) {
49 | if (target.endsWith(".")) {
50 | this.target = target;
51 | } else {
52 | Log.w(TAG, "User missed the point by giving a relative domain. Using absolute domain...");
53 | this.target = target + ".";
54 | }
55 | }
56 |
57 | /**
58 | * Retrieve the DNS servers specified in the Android device configuration.
59 | *
60 | * @return A list of DNS servers.
61 | */
62 | private static String[] retrieveDeviceServers() {
63 | List servers = new ArrayList<>();
64 | try {
65 | Class> SystemProperties = Class.forName("android.os.SystemProperties");
66 | Method method = SystemProperties.getMethod("get", String.class);
67 | for (String name : new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4",}) {
68 | String value = (String) method.invoke(null, name);
69 | if (value != null && !"".equals(value) && !servers.contains(value))
70 | servers.add(value);
71 | }
72 | } catch (Exception ex) {
73 | Log.d(TAG, "Unable to get local DNS resolver");
74 | }
75 | return servers.toArray(new String[0]);
76 | }
77 |
78 | @Override
79 | public String toString() {
80 | return "DNSLookupConfig{" +
81 | "target='" + target + '\'' +
82 | ", server='" + server + '\'' +
83 | ", qclass='" + qclass + '\'' +
84 | ", qtype='" + qtype + '\'' +
85 | '}';
86 | }
87 |
88 | public String getTarget() {
89 | return target;
90 | }
91 |
92 | public String getServer() {
93 | return server;
94 | }
95 |
96 | public void setServer(String server) {
97 | this.server = server;
98 | }
99 |
100 | public String getQclass() {
101 | return qclass;
102 | }
103 |
104 | public void setQclass(String qclass) {
105 | this.qclass = qclass;
106 | }
107 |
108 | public String getQtype() {
109 | return qtype;
110 | }
111 |
112 | public void setQtype(String qtype) {
113 | this.qtype = qtype;
114 | }
115 |
116 | public boolean isForceTCP() {
117 | return forceTCP;
118 | }
119 |
120 | public void setForceTCP(boolean forceTCP) {
121 | this.forceTCP = forceTCP;
122 | }
123 |
124 | public int getTimeout() {
125 | return timeout;
126 | }
127 |
128 | public void setTimeout(int timeout) {
129 | this.timeout = timeout;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/tcp/TCPThroughputTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.tcp;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.Closeable;
6 | import java.io.IOException;
7 | import java.net.InetAddress;
8 | import java.net.InetSocketAddress;
9 | import java.net.Socket;
10 | import java.net.SocketAddress;
11 | import java.util.ArrayList;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.Random;
15 |
16 | import io.apisense.network.Measurement;
17 | import io.apisense.network.MeasurementError;
18 |
19 | /**
20 | * Abstract class containing common code used for UDP upload and download tests
21 | */
22 | abstract class TCPThroughputTask extends Measurement {
23 | protected static final int BUFFER_SIZE = 5000;
24 | private static final String TAG = "TCPThroughputTask";
25 | private static final int SEC_TO_MS = 1000;
26 | protected final TCPThroughputConfig config;
27 |
28 | // helper variables
29 | protected int accumulativeSize = 0;
30 | //start time of each sampling period in milliseconds
31 | protected long startSampleTime = 0;
32 | protected long taskStartTime = 0;
33 | protected long taskEndTime = 0;
34 | /**
35 | * Data consummed (sent/received) after slow star period (in bits)
36 | */
37 | protected long dataConsumedAfterSlowStart = 0;
38 | List samplingResults = new ArrayList<>();
39 |
40 | TCPThroughputTask(String taskName, TCPThroughputConfig tcpThroughputConfig) {
41 | super(taskName);
42 | config = tcpThroughputConfig;
43 | }
44 |
45 | protected Socket buildUpSocket(InetAddress hostname, int portNum) throws MeasurementError {
46 | try {
47 | Socket tcpSocket = new Socket();
48 | SocketAddress remoteAddr = new InetSocketAddress(hostname, portNum);
49 | tcpSocket.connect(remoteAddr, config.getTcpTimeoutSec() * SEC_TO_MS);
50 | tcpSocket.setSoTimeout(config.getTcpTimeoutSec() * SEC_TO_MS);
51 | tcpSocket.setTcpNoDelay(true);
52 | return tcpSocket;
53 | } catch (IOException e) {
54 | throw new MeasurementError(taskName, "Error opening socket at " + hostname + ":" + portNum, e);
55 | }
56 | }
57 |
58 | /**
59 | * update the total received packet size
60 | *
61 | * @param delta time period increment
62 | */
63 | protected void updateSize(int delta) {
64 | double gtime = System.currentTimeMillis() - this.taskStartTime;
65 | //ignore slow start
66 | if (gtime < config.getSlowStartPeriodSec() * SEC_TO_MS) {
67 | return;
68 | }
69 | if (this.startSampleTime == 0) {
70 | this.startSampleTime = System.currentTimeMillis();
71 | this.accumulativeSize = 0;
72 | }
73 | this.dataConsumedAfterSlowStart += delta;
74 | this.accumulativeSize += delta;
75 | double time = System.currentTimeMillis() - this.startSampleTime;
76 | if (time >= (config.getSamplePeriodSec() * SEC_TO_MS)) {
77 | double throughput = (double) this.accumulativeSize * 1000.0 / time; //in bits/second
78 | this.addSamplingResult(throughput);
79 | this.accumulativeSize = 0;
80 | this.startSampleTime = System.currentTimeMillis();
81 | }
82 | }
83 |
84 | protected void addSamplingResult(double item) {
85 | samplingResults.add(item);
86 | Collections.sort(samplingResults);
87 | }
88 |
89 | /**
90 | * Fills up an array with random bytes
91 | *
92 | * @param byteArray Array to fill with random bytes
93 | */
94 | protected void genRandomByteArray(byte[] byteArray) {
95 | Random randStr = new Random();
96 | for (int i = 0; i < byteArray.length; i++) {
97 | byteArray[i] = (byte) ('a' + randStr.nextInt(26));
98 | }
99 | }
100 |
101 |
102 | /**
103 | * Actually close the given stream, logging a warning
104 | * if anything wrong occured.
105 | *
106 | * Cannot use {@link TCPThroughputTask#closeStream(Closeable)}
107 | * since it requires a cast available since API 19.
108 | *
109 | * @param socket The socket to close.
110 | */
111 | protected void closeSocket(Socket socket) {
112 | try {
113 | if (socket != null) {
114 | socket.close();
115 | }
116 | } catch (IOException e) {
117 | Log.w(TAG, "Fail while closing socket", e);
118 | }
119 | }
120 |
121 | /**
122 | * Actually close the given stream, logging a warning
123 | * if anything wrong occured.
124 | *
125 | * @param stream The closeable to close.
126 | */
127 | protected void closeStream(Closeable stream) {
128 | try {
129 | if (stream != null) {
130 | stream.close();
131 | }
132 | } catch (IOException e) {
133 | Log.w(TAG, "Fail while closing stream", e);
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/tcp/TCPUploadTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.tcp;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.OutputStream;
8 | import java.net.Socket;
9 |
10 | import io.apisense.network.MeasurementError;
11 | import io.apisense.network.MeasurementResult;
12 |
13 | /**
14 | * Measurement class used to realise an TCP upload test
15 | */
16 | public class TCPUploadTask extends TCPThroughputTask {
17 | private static final String TAG = "TCPUploadTask";
18 | private static final int PORT_UPLINK = 6002;
19 | private static final String UPLINK_FINISH_MSG = "*";
20 | private static final long MB_TO_B = 1048576;
21 |
22 | public TCPUploadTask(TCPThroughputConfig tcpThroughputConfig) {
23 | super(TAG, tcpThroughputConfig);
24 | }
25 |
26 | /**
27 | * {@inheritDoc}
28 | *
29 | * @return A {@link TCPThroughputResult} object containing information on the TCP upload test.
30 | * @throws MeasurementError {@inheritDoc}
31 | */
32 | public MeasurementResult execute() throws MeasurementError {
33 | Log.d(TAG, "Start");
34 | Socket tcpSocket = buildUpSocket(config.getTarget(), PORT_UPLINK);
35 |
36 | OutputStream oStream;
37 | InputStream iStream;
38 |
39 | try {
40 | oStream = tcpSocket.getOutputStream();
41 | iStream = tcpSocket.getInputStream();
42 | } catch (IOException e) {
43 | throw new MeasurementError(taskName, "Unable to open stream", e);
44 | }
45 |
46 |
47 | this.taskStartTime = System.currentTimeMillis();
48 | sendData(oStream);
49 | this.taskEndTime = System.currentTimeMillis();
50 | Log.d(TAG, "Finished");
51 |
52 | try {
53 | retrieveResult(iStream);
54 | } catch (OutOfMemoryError e) { // TODO: See if this catch clause is really necessary
55 | throw new MeasurementError(taskName, "Detect out of memory during Uplink task.", e);
56 | } finally {
57 | closeStream(oStream);
58 | closeStream(iStream);
59 | closeSocket(tcpSocket);
60 | }
61 | return new TCPThroughputResult(TAG, this.taskStartTime, this.taskEndTime,
62 | config, this.samplingResults, this.dataConsumedAfterSlowStart);
63 | }
64 |
65 | /**
66 | * Update samplingResults with the received packet.
67 | *
68 | * @param iStream The input stream to receive the result packet from.
69 | * @throws MeasurementError If the interaction with stream fails.
70 | */
71 | private void retrieveResult(InputStream iStream) throws MeasurementError {
72 | String message;
73 | int resultMsgLen;
74 | try {
75 | // read from server side results
76 | byte[] resultMsg = new byte[BUFFER_SIZE];
77 | resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length);
78 | message = new String(resultMsg);
79 | } catch (IOException e) {
80 | throw new MeasurementError(taskName, "Unable to retrieve upload result", e);
81 | }
82 |
83 | if (!message.isEmpty() && resultMsgLen > 0) {
84 | String resultMsgStr = message.substring(0, resultMsgLen);
85 | // Sample result string is "1111.11#2222.22#3333.33";
86 | String[] results = resultMsgStr.split("#");
87 | for (String result : results) {
88 | this.addSamplingResult(Double.valueOf(result));
89 | }
90 | }
91 | }
92 |
93 | /**
94 | * Send the required quantity of data on the given {@link Socket}.
95 | *
96 | * @param oStream The output stream to write onto.
97 | * @return The quantity of sent bytes.
98 | * @throws MeasurementError If the interaction with stream fails.
99 | */
100 | private long sendData(OutputStream oStream) throws MeasurementError {
101 | byte[] uplinkBuffer = new byte[config.getPktSizeUpBytes()];
102 | this.genRandomByteArray(uplinkBuffer);
103 | long pktSizeSent = 0;
104 |
105 | try {
106 | do {
107 | sendMessageOnStream(uplinkBuffer, oStream);
108 | pktSizeSent += config.getPktSizeUpBytes();
109 | } while (pktSizeSent < config.getDataLimitMbUp() * MB_TO_B);
110 |
111 | // send last message with special content
112 | sendMessageOnStream(TCPUploadTask.UPLINK_FINISH_MSG.getBytes(), oStream);
113 | } catch (IOException e) {
114 | throw new MeasurementError(taskName, "Unable to upload data", e);
115 | }
116 | return pktSizeSent;
117 | }
118 |
119 | /**
120 | * Write a message on the given {@link OutputStream}
121 | *
122 | * @param message The byte array to write.
123 | * @param stream The stream to write onto.
124 | * @throws IOException If the interaction with stream fails.
125 | */
126 | private void sendMessageOnStream(byte[] message, OutputStream stream) throws IOException {
127 | stream.write(message, 0, message.length);
128 | stream.flush();
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/MLabNS.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network;
2 |
3 |
4 | import android.support.annotation.NonNull;
5 | import android.util.Log;
6 |
7 | import org.json.JSONArray;
8 | import org.json.JSONException;
9 | import org.json.JSONObject;
10 |
11 | import java.io.BufferedReader;
12 | import java.io.IOException;
13 | import java.io.InputStream;
14 | import java.io.InputStreamReader;
15 | import java.net.HttpURLConnection;
16 | import java.net.SocketTimeoutException;
17 | import java.net.URL;
18 | import java.security.InvalidParameterException;
19 | import java.util.ArrayList;
20 | import java.util.List;
21 |
22 | /**
23 | * Determines the closest MLab server.
24 | *
25 | * @see https://www.measurementlab.net/
26 | */
27 | public class MLabNS implements Runnable {
28 | private static final String TAG = "MLabNS";
29 | private static final String MLAB_URL = "http://mlab-ns.appspot.com/mobiperf?format=json";
30 | private static final String IP_FIELD = "ip";
31 | private final MLabListener callback;
32 |
33 | public MLabNS(MLabListener listener) {
34 | this.callback = listener;
35 | }
36 |
37 |
38 | @Override
39 | public void run() {
40 | callback.onMLabFinished(retrieveMLabIPs());
41 | }
42 |
43 | /**
44 | * Returns an {@link List} containing IPV4/IPV6 addresses of MLab server to run a
45 | * TCP or UDP Test.
46 | *
47 | * @return List of IP addresses to run TCP/UDP tests
48 | */
49 | public static List retrieveMLabIPs() {
50 | ArrayList mlabNSResult = new ArrayList<>();
51 | String response;
52 | HttpURLConnection con = null;
53 | InputStream inputStream = null;
54 | try {
55 | URL target = new URL(MLAB_URL);
56 | con = (HttpURLConnection) target.openConnection();
57 | con.setRequestMethod("GET");
58 | int responseCode = con.getResponseCode();
59 |
60 | if (responseCode != 200) {
61 | throw new InvalidParameterException("Received status " + responseCode + " from mlab-ns");
62 | }
63 |
64 | inputStream = con.getInputStream();
65 | response = getResponseString(inputStream);
66 | } catch (SocketTimeoutException e) {
67 | throw new InvalidParameterException("Connect to m-lab-ns timeout. Please try again.");
68 | } catch (IOException e) {
69 | throw new InvalidParameterException(e.getMessage());
70 | } finally {
71 | if (inputStream != null) {
72 | try {
73 | inputStream.close();
74 | } catch (IOException e) {
75 | Log.e(TAG, "Error while closing stream", e);
76 | }
77 | }
78 | if (con != null) {
79 | con.disconnect();
80 | }
81 | }
82 | mlabNSResult.addAll(retrieveIps(response));
83 | return mlabNSResult;
84 | }
85 |
86 | /**
87 | * Read the given stream to retrieve the response body
88 | * and return it as a String.
89 | *
90 | * @param inputStream The stream to read
91 | * @return The body as String.
92 | * @throws IOException If the stream interaction fails.
93 | */
94 | @NonNull
95 | private static String getResponseString(InputStream inputStream) throws IOException {
96 | BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
97 | String inputLine;
98 | StringBuffer response = new StringBuffer();
99 |
100 | while ((inputLine = in.readLine()) != null) {
101 | response.append(inputLine);
102 | }
103 | in.close();
104 |
105 | return response.toString();
106 | }
107 |
108 | /**
109 | * Parse the response Json to retrieve the server IPs.
110 | *
111 | * @param response The String representation of the server response.
112 | * @return The list of available IPs.
113 | */
114 | @NonNull
115 | private static List retrieveIps(String response) {
116 | List result = new ArrayList<>();
117 | try {
118 | JSONObject json = new JSONObject(response);
119 | if (json.get(IP_FIELD) instanceof JSONArray) {
120 | // Convert array value into ArrayList
121 | JSONArray jsonArray = null;
122 | jsonArray = (JSONArray) json.get(IP_FIELD);
123 | for (int i = 0; i < jsonArray.length(); i++) {
124 | result.add(jsonArray.get(i).toString());
125 | }
126 | } else if (json.get(IP_FIELD) instanceof String) {
127 | // Append the string into ArrayList
128 | result.add(String.valueOf(json.getString(IP_FIELD)));
129 | } else {
130 | throw new InvalidParameterException("Unknown type " +
131 | json.get(IP_FIELD).getClass().toString() + " of value " + json.get(IP_FIELD));
132 | }
133 | } catch (JSONException e) {
134 | throw new InvalidParameterException(e.getMessage());
135 | }
136 | return result;
137 | }
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/udp/UDPPacket.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.udp;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.DataInputStream;
6 | import java.io.DataOutputStream;
7 | import java.io.IOException;
8 | import java.net.DatagramPacket;
9 | import java.net.InetAddress;
10 |
11 | import io.apisense.network.MeasurementError;
12 |
13 | /**
14 | * @author Hongyi Yao (hyyao@umich.edu) A helper structure for packing and unpacking network
15 | * message
16 | */
17 | final class UDPPacket {
18 | static final int PKT_ERROR = 1;
19 | static final int PKT_RESPONSE = 2;
20 | static final int PKT_DATA = 3;
21 | static final int PKT_REQUEST = 4;
22 |
23 | /**
24 | * Name of the task to referecence in case of a {@link MeasurementError}.
25 | */
26 | private final String taskName;
27 |
28 | /**
29 | * Type of the {@link UDPPacket},
30 | * may be {@link UDPPacket#PKT_ERROR}, {@link UDPPacket#PKT_RESPONSE},
31 | * {@link UDPPacket#PKT_DATA}, or {@link UDPPacket#PKT_REQUEST}
32 | */
33 | public final int type;
34 |
35 | /**
36 | * Number of burst to send.
37 | */
38 | public final int burstCount;
39 | /**
40 | * Number of packet received in a wrong order
41 | */
42 | public final int outOfOrderNum;
43 | /**
44 | * Data packet: local timestamp
45 | * Response packet: jitter
46 | */
47 | public final long timestamp;
48 | /**
49 | * Size of each UDP packet to send.
50 | */
51 | public final int packetSize;
52 | /**
53 | * Request sequence number.
54 | */
55 | public final int seq;
56 | /**
57 | * Time to wait between each packet.
58 | */
59 | public final int udpInterval;
60 | /**
61 | * Identification of the packet,
62 | * determine its order in the sequence.
63 | */
64 | public int packetNum;
65 |
66 | /**
67 | * Build from scratch an {@link UDPPacket} from the given configuration.
68 | *
69 | * @param taskName Name of the task to referecence in case of a {@link MeasurementError}
70 | * @param type Type of packet to build.
71 | * @param config Burst configuration to set in the packet.
72 | */
73 | public UDPPacket(String taskName, int type, UDPBurstConfig config) {
74 | this.taskName = taskName;
75 | this.type = type;
76 | this.burstCount = config.getUdpBurstCount();
77 | this.packetSize = config.getPacketSizeByte();
78 | this.udpInterval = (int) Math.ceil(config.getUdpInterval() / 1000); // convert µs to ms
79 | this.seq = 0;
80 |
81 | // Unrelevant properties
82 | outOfOrderNum = 0;
83 | timestamp = System.currentTimeMillis();
84 |
85 | }
86 |
87 | /**
88 | * Unpack received message and fill the structure
89 | *
90 | * @param taskName Name of the task to referecence in case of a {@link MeasurementError}
91 | * @param rawdata Network message
92 | * @throws MeasurementError stream reader failed
93 | */
94 | public UDPPacket(String taskName, byte[] rawdata) throws MeasurementError {
95 | this.taskName = taskName;
96 | ByteArrayInputStream byteIn = new ByteArrayInputStream(rawdata);
97 | DataInputStream dataIn = new DataInputStream(byteIn);
98 |
99 | try {
100 | type = dataIn.readInt();
101 | burstCount = dataIn.readInt();
102 | packetNum = dataIn.readInt();
103 | outOfOrderNum = dataIn.readInt();
104 | timestamp = dataIn.readLong();
105 | packetSize = dataIn.readInt();
106 | seq = dataIn.readInt();
107 | udpInterval = dataIn.readInt();
108 | } catch (IOException e) {
109 | throw new MeasurementError(taskName, "Fetch payload failed! " + e.getMessage());
110 | }
111 |
112 | try {
113 | byteIn.close();
114 | } catch (IOException e) {
115 | throw new MeasurementError(taskName, "Error closing inputstream!");
116 | }
117 | }
118 |
119 | public DatagramPacket createDatagram(InetAddress target, int port) throws MeasurementError {
120 | byte[] data = getByteArray();
121 | return new DatagramPacket(data, data.length, target, port);
122 | }
123 |
124 | /**
125 | * Pack the structure to the network message
126 | *
127 | * @return the network message in byte[]
128 | * @throws MeasurementError stream writer failed
129 | */
130 | public byte[] getByteArray() throws MeasurementError {
131 |
132 | ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
133 | DataOutputStream dataOut = new DataOutputStream(byteOut);
134 |
135 | try {
136 | dataOut.writeInt(type);
137 | dataOut.writeInt(burstCount);
138 | dataOut.writeInt(packetNum);
139 | dataOut.writeInt(outOfOrderNum);
140 | dataOut.writeLong(timestamp);
141 | dataOut.writeInt(packetSize);
142 | dataOut.writeInt(seq);
143 | dataOut.writeInt(udpInterval);
144 | } catch (IOException e) {
145 | throw new MeasurementError(taskName, "Create rawpacket failed! " + e.getMessage());
146 | }
147 |
148 | byte[] rawPacket = byteOut.toByteArray();
149 |
150 | try {
151 | byteOut.close();
152 | } catch (IOException e) {
153 | throw new MeasurementError(taskName, "Error closing outputstream!");
154 | }
155 | return rawPacket;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/dns/DNSLookupTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.dns;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.util.Log;
5 |
6 | import org.xbill.DNS.DClass;
7 | import org.xbill.DNS.DNSClient;
8 | import org.xbill.DNS.Flags;
9 | import org.xbill.DNS.Message;
10 | import org.xbill.DNS.Name;
11 | import org.xbill.DNS.OPTRecord;
12 | import org.xbill.DNS.PublicTCPClient;
13 | import org.xbill.DNS.PublicUDPClient;
14 | import org.xbill.DNS.Record;
15 | import org.xbill.DNS.TextParseException;
16 | import org.xbill.DNS.Type;
17 |
18 | import java.io.IOException;
19 | import java.net.InetSocketAddress;
20 | import java.net.SocketAddress;
21 |
22 | import io.apisense.network.Measurement;
23 | import io.apisense.network.MeasurementError;
24 |
25 | /**
26 | * Measures the DNS lookup time
27 | */
28 | public class DNSLookupTask extends Measurement {
29 | public static final String TAG = "DNSLookup";
30 | private final DNSLookupConfig config;
31 |
32 | public DNSLookupTask(DNSLookupConfig config) {
33 | super(TAG);
34 | this.config = config;
35 | }
36 |
37 | /**
38 | * Parse the raw response bytes to a wrapped dns answer.
39 | *
40 | * @param useTCP If the client is currently using TCP.
41 | * @param respBytes The raw response bytes.
42 | * @return The parsed {@link Message}.
43 | * @throws MeasurementError If the response could not be parsed.
44 | * @throws TruncatedException If the response is truncated while client is using UDP.
45 | */
46 | @NonNull
47 | private Message parseMessage(boolean useTCP, byte[] respBytes)
48 | throws TruncatedException, MeasurementError {
49 | Message response;
50 | try {
51 | response = new Message(respBytes);
52 | Log.d(TAG, "Successfully parsed response");
53 | // if the response was truncated, then re-query over TCP
54 | if (!useTCP && response.getHeader().getFlag(Flags.TC)) {
55 | throw new TruncatedException();
56 | }
57 | } catch (IOException e) {
58 | throw new MeasurementError(taskName, "Problem trying to parse dns packet", e);
59 | }
60 | return response;
61 | }
62 |
63 | /**
64 | * Initialize and connect the dns TCP or UDP client.
65 | *
66 | * @param server The server to connect the client to.
67 | * @param useTCP Tells if the client should be TCP or UDP.
68 | * @param endTime The request timeout.
69 | * @return The initialized client.
70 | * @throws MeasurementError If any error occurred during client creation or connection.
71 | */
72 | @NonNull
73 | private DNSClient connectClient(String server, boolean useTCP, long endTime)
74 | throws MeasurementError {
75 | DNSClient client;
76 | try {
77 | if (useTCP) {
78 | client = new PublicTCPClient(endTime);
79 | } else {
80 | client = new PublicUDPClient(endTime);
81 | client.bind(null);
82 | }
83 | SocketAddress addr = new InetSocketAddress(server, 53);
84 | client.connect(addr);
85 | } catch (IOException e) {
86 | throw new MeasurementError(taskName, "Error while creating client", e);
87 | }
88 | Log.d(TAG, "Initialized client");
89 | return client;
90 | }
91 |
92 | /**
93 | * Send a request to the DNS client and return
94 | * the time when request was sent.
95 | *
96 | * @param client The client to send query on.
97 | * @param output The raw query to send.
98 | * @return The timestamp when request was successfully sent, -1 if unsuccessful.
99 | */
100 | private static long sendRequest(DNSClient client, byte[] output) {
101 | try {
102 | client.send(output);
103 | } catch (IOException e) {
104 | Log.e(TAG, "Error while sending DNS request", e);
105 | return -1;
106 | }
107 | return System.currentTimeMillis();
108 | }
109 |
110 | /**
111 | * Receive and return a DNS response from the server.
112 | *
113 | * @param client The client to receive response from.
114 | * @param udpSize The maximum size of the response if UDP.
115 | * @return The raw response in a byte array, the array will be empty if nothing has been received.
116 | */
117 | @NonNull
118 | private static byte[] receiveResponse(DNSClient client, int udpSize) {
119 | byte[] in = {};
120 | try {
121 | in = client.recv(udpSize);
122 | } catch (IOException e) {
123 | Log.d(TAG, "Problem while receiving packet ", e);
124 | }
125 | return in;
126 | }
127 |
128 | @Override
129 | public DNSLookupResult execute() throws MeasurementError {
130 | Log.d(TAG, "Running DNS lookup with configuration: " + config);
131 | Record question;
132 | try {
133 | question = Record.newRecord(Name.fromString(config.getTarget()),
134 | Type.value(config.getQtype()), DClass.value(config.getQclass()));
135 | } catch (TextParseException e) {
136 | throw new MeasurementError("Error constructing packet", e);
137 | }
138 | Message query = Message.newQuery(question);
139 | Log.v(TAG, "Constructed question: " + question);
140 | Log.v(TAG, "Constructed query: " + query);
141 | return sendMeasurement(query, config.isForceTCP());
142 | }
143 |
144 | /**
145 | * Put the query on the wire and wait for responses.
146 | *
147 | * @param query The DNS query to send to the server.
148 | * @param forceTCP Tells whether we should force request to use TCP or not.
149 | * @return The lookup result.
150 | * @throws MeasurementError If anything goes wrong during DNS lookup.
151 | */
152 | @NonNull
153 | private DNSLookupResult sendMeasurement(Message query, boolean forceTCP) throws MeasurementError {
154 | byte[] output = query.toWire();
155 | OPTRecord opt = query.getOPT();
156 |
157 | int udpSize = opt != null ? opt.getPayloadSize() : 512;
158 | boolean useTCP = forceTCP || (output.length > udpSize);
159 | long timeout = System.currentTimeMillis() + config.getTimeout();
160 |
161 | DNSClient client = connectClient(config.getServer(), useTCP, timeout);
162 |
163 | // Sending request
164 | long startTime;
165 | do {
166 | startTime = sendRequest(client, output);
167 | } while (startTime == -1 && System.currentTimeMillis() < timeout);
168 |
169 | // Retrieving result
170 | byte[] respBytes;
171 | do {
172 | respBytes = receiveResponse(client, udpSize);
173 | } while (respBytes.length == 0 && System.currentTimeMillis() < timeout);
174 | long endTime = System.currentTimeMillis();
175 |
176 | // Parsing response
177 | DNSLookupResult result;
178 | try {
179 | result = DNSLookupResult.fromMessage(config, parseMessage(useTCP, respBytes), startTime, endTime);
180 | } catch (TruncatedException e) {
181 | Log.d(TAG, "UDP response truncated, re-querying over TCP");
182 | try {
183 | client.cleanup();
184 | } catch (IOException err) {
185 | Log.w(TAG, "Unable to clean client while retrying over TCP", e);
186 | }
187 | return sendMeasurement(query, true);
188 | }
189 |
190 | return result;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/measures/src/main/java/io/apisense/network/ping/ICMPTask.java:
--------------------------------------------------------------------------------
1 | package io.apisense.network.ping;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.util.Log;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.io.InputStreamReader;
10 | import java.util.Locale;
11 | import java.util.regex.Matcher;
12 | import java.util.regex.Pattern;
13 |
14 | import io.apisense.network.Measurement;
15 | import io.apisense.network.MeasurementError;
16 |
17 | /**
18 | * Measurement class used to realise a Traceroute
19 | */
20 | public class ICMPTask extends Measurement {
21 | public static final String TAG = "PING";
22 |
23 | private static final String REGEX_TTL_EXCEEDED = "icmp_seq=\\d+ Time to live exceeded";
24 | private static final String REGEX_SUCCESS = "icmp_seq=\\d+ ttl=\\d+ time=(\\d+.\\d+) ms";
25 | private static final String REGEX_RTT_SUCCESS = "(\\d+.\\d+)/(\\d+.\\d+)/(\\d+.\\d+)/(\\d+.\\d+) ms";
26 |
27 | /**
28 | * Template: From *ip*: icmp_seq=1 Time to live exceeded
29 | */
30 | private static final Pattern PING_RESPONSE_TTL_EXCEEDED_NO_HOSTNAME = Pattern.compile(
31 | "From ([\\d.]+): " + REGEX_TTL_EXCEEDED);
32 |
33 | /**
34 | * Template:
35 | * 64 bytes from *ip*: icmp_seq=1 ttl=*ttl* time=*latency* ms
36 | */
37 | private static final Pattern PING_RESPONSE_SUCCESS_NO_HOSTNAME = Pattern.compile(
38 | "\\d+ bytes from ([\\d.]+): " + REGEX_SUCCESS);
39 |
40 | /**
41 | * Template:
42 | * From *hostname* (*ip*): icmp_seq=1 Time to live exceeded
43 | */
44 | private static final Pattern PING_RESPONSE_TTL_EXCEEDED = Pattern.compile(
45 | "From ([\\w\\d\\-.]+) \\(([\\d.]+)\\): " + REGEX_TTL_EXCEEDED);
46 |
47 | /**
48 | * Template:
49 | * 64 bytes from *hostname* (*ip*): icmp_seq=1 ttl=*ttl* time=*latency* ms
50 | */
51 | private static final Pattern PING_RESPONSE_SUCCESS = Pattern.compile(
52 | "\\d+ bytes from ([\\w\\d\\-.]+) \\(([\\d.]+)\\): " + REGEX_SUCCESS);
53 |
54 | /**
55 | * Template:
56 | * round-trip min/avg/max/stddev = *rtt.min*\/*rtt.avg*\/*rtt.max*\/0.114 ms
57 | */
58 | private static final Pattern PING_RESPONSE_RTT_SUCCESS = Pattern.compile(
59 | "rtt min/avg/max/mdev = " + REGEX_RTT_SUCCESS);
60 |
61 | /**
62 | * This message is displayed when all packets are lost.
63 | *
64 | * Template:
65 | * 1 packets transmitted, 0 received, 100% packet loss, time 0ms
66 | */
67 | private static final Pattern PING_RESPONSE_TIMEOUT = Pattern.compile(
68 | "\\d+ packets transmitted, 0 received, 100% packet loss, time \\d+ms"
69 | );
70 |
71 | private ICMPConfig config;
72 |
73 | public ICMPTask(ICMPConfig config) {
74 | super(TAG);
75 | this.config = config;
76 | }
77 |
78 | /**
79 | * *Synchronous* Ping request with a specific ttl.
80 | *
81 | * @param url The url to set on the command request.
82 | * @param ttl The TTL to set on the command request.
83 | * @return The ping command to execute.
84 | */
85 | private static String generatePingCommand(String url, int ttl) {
86 | String format = "ping -c 1 -t %d ";
87 | return String.format(Locale.US, format, ttl) + url;
88 | }
89 |
90 | /**
91 | * Actual generatePingCommand request and response parsing.
92 | *
93 | * @param command The command to execute for this ping.
94 | * @return The {@link ICMPResult} value.
95 | * @throws ICMPTask.PINGException If the ping execution fails.
96 | * @throws IOException
97 | * @throws InterruptedException
98 | */
99 | private ICMPResult launchPingCommand(String command) throws PINGException, IOException, InterruptedException {
100 | Log.d(TAG, "Will launch : " + command);
101 | long startTime = System.currentTimeMillis();
102 | Process p = Runtime.getRuntime().exec(command);
103 |
104 | BufferedReader stdin = new BufferedReader(new InputStreamReader(p.getInputStream()));
105 | if (p.waitFor() >= 2) {
106 | throw new ICMPTask.PINGException(p.getErrorStream());
107 | } else {
108 | return parsePingResponse(stdin, startTime);
109 | }
110 | }
111 |
112 | /**
113 | * Retrieve every possible information about the Ping.
114 | *
115 | * @param stdin The ping output.
116 | * @param startTimeMs The task start timestamp in millisecond.
117 | * @return The built result from output.
118 | * @throws IOException
119 | */
120 | private ICMPResult parsePingResponse(BufferedReader stdin, long startTimeMs) throws IOException, PINGException {
121 | String hostname = null;
122 | String ip = null;
123 | Rtt rtt;
124 | long endTime = System.currentTimeMillis();
125 | long latency = endTime - startTimeMs;
126 | int currentTtl = config.getTtl();
127 |
128 | String line;
129 | Matcher matcher;
130 |
131 | while ((line = stdin.readLine()) != null) {
132 | Log.d(TAG, "Parsing line : " + line);
133 |
134 | matcher = PING_RESPONSE_TTL_EXCEEDED.matcher(line);
135 |
136 | if (matcher.matches()) {
137 | hostname = matcher.group(1);
138 | ip = matcher.group(2);
139 | return new ICMPResult(startTimeMs, endTime, config, hostname, ip, latency, currentTtl, null);
140 | }
141 |
142 | matcher = PING_RESPONSE_TTL_EXCEEDED_NO_HOSTNAME.matcher(line);
143 |
144 | if (matcher.matches()) {
145 | ip = matcher.group(1);
146 | return new ICMPResult(startTimeMs, endTime, config, null, ip, latency, currentTtl, null);
147 | }
148 |
149 | matcher = PING_RESPONSE_SUCCESS.matcher(line);
150 |
151 | if (matcher.matches()) {
152 | hostname = matcher.group(1);
153 | ip = matcher.group(2);
154 | latency = Float.valueOf(matcher.group(3)).longValue(); // milliseconds
155 | }
156 |
157 | matcher = PING_RESPONSE_SUCCESS_NO_HOSTNAME.matcher(line);
158 |
159 | if (matcher.matches()) {
160 | ip = matcher.group(1);
161 | latency = Float.valueOf(matcher.group(2)).longValue(); // milliseconds
162 | }
163 |
164 | matcher = PING_RESPONSE_RTT_SUCCESS.matcher(line);
165 |
166 | if (matcher.matches()) {
167 | float min = Float.valueOf(matcher.group(1));
168 | float avg = Float.valueOf(matcher.group(2));
169 | float max = Float.valueOf(matcher.group(3));
170 | float mdev = Float.valueOf(matcher.group(4));
171 |
172 | rtt = new Rtt(min, avg, max, mdev);
173 |
174 | return new ICMPResult(startTimeMs, endTime, config, hostname, ip, latency, currentTtl, rtt);
175 | }
176 |
177 | matcher = PING_RESPONSE_TIMEOUT.matcher(line);
178 |
179 | if (matcher.matches()) {
180 | throw new PINGException("Packet is lost");
181 | }
182 | }
183 |
184 | throw new PINGException("Could not parse response");
185 | }
186 |
187 | @Override
188 | @NonNull
189 | public ICMPResult execute() throws MeasurementError {
190 | try {
191 | String command = generatePingCommand(config.getUrl(), config.getTtl());
192 | return launchPingCommand(command);
193 | } catch (PINGException | IOException | InterruptedException e) {
194 | throw new MeasurementError(taskName, e);
195 | }
196 | }
197 |
198 | /**
199 | * Exception thrown from a PING task
200 | */
201 | private static class PINGException extends Exception {
202 | PINGException(InputStream errorStream) {
203 | super(buildErrorMessage(errorStream));
204 | }
205 |
206 | PINGException(String message) {
207 | super(message);
208 | }
209 |
210 | @NonNull
211 | private static String buildErrorMessage(InputStream errorStream) {
212 | BufferedReader stderr = new BufferedReader(new InputStreamReader(errorStream));
213 | String nextLine = "";
214 | String message = "";
215 | while (nextLine != null) {
216 | message += nextLine;
217 | try {
218 | nextLine = stderr.readLine();
219 | } catch (IOException e) {
220 | Log.w(TAG, "Error message creation interupted.", e);
221 | break; // Stop reading and returns what we had until now.
222 | }
223 | }
224 | return message;
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------