diff --git a/Mobilyzer/build.gradle b/Mobilyzer/build.gradle new file mode 100644 index 0000000..ce91cca --- /dev/null +++ b/Mobilyzer/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 19 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 19 + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + java.srcDirs = ['src'] + } + } + + lintOptions { + abortOnError false + } + +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.google.android.gms:play-services:7.3.0' + compile project(':mobilibs:ExoPlayer:library') +} \ No newline at end of file diff --git a/Mobilyzer/jniLibs/armeabi-v7a/libcronet.so b/Mobilyzer/jniLibs/armeabi-v7a/libcronet.so new file mode 100755 index 0000000..cfe39e0 Binary files /dev/null and b/Mobilyzer/jniLibs/armeabi-v7a/libcronet.so differ diff --git a/Mobilyzer/libs/android-support-v4.jar b/Mobilyzer/libs/android-support-v4.jar deleted file mode 100644 index 9056828..0000000 Binary files a/Mobilyzer/libs/android-support-v4.jar and /dev/null differ diff --git a/Mobilyzer/libs/cronet_api.jar b/Mobilyzer/libs/cronet_api.jar new file mode 100644 index 0000000..f10256a Binary files /dev/null and b/Mobilyzer/libs/cronet_api.jar differ diff --git a/Mobilyzer/libs/cronet_java.jar b/Mobilyzer/libs/cronet_java.jar new file mode 100644 index 0000000..6bc2e63 Binary files /dev/null and b/Mobilyzer/libs/cronet_java.jar differ diff --git a/Mobilyzer/quiclibs/base_java.jar b/Mobilyzer/quiclibs/base_java.jar new file mode 100644 index 0000000..9737fa4 Binary files /dev/null and b/Mobilyzer/quiclibs/base_java.jar differ diff --git a/Mobilyzer/quiclibs/jsr_305_javalib.jar b/Mobilyzer/quiclibs/jsr_305_javalib.jar new file mode 100644 index 0000000..7174522 Binary files /dev/null and b/Mobilyzer/quiclibs/jsr_305_javalib.jar differ diff --git a/Mobilyzer/quiclibs/net_java.jar b/Mobilyzer/quiclibs/net_java.jar new file mode 100644 index 0000000..980fb1f Binary files /dev/null and b/Mobilyzer/quiclibs/net_java.jar differ diff --git a/Mobilyzer/quiclibs/url_java.jar b/Mobilyzer/quiclibs/url_java.jar new file mode 100644 index 0000000..c2a48fa Binary files /dev/null and b/Mobilyzer/quiclibs/url_java.jar differ diff --git a/Mobilyzer/src/com/mobilyzer/MeasurementResult.java b/Mobilyzer/src/com/mobilyzer/MeasurementResult.java index ade3355..7704d29 100755 --- a/Mobilyzer/src/com/mobilyzer/MeasurementResult.java +++ b/Mobilyzer/src/com/mobilyzer/MeasurementResult.java @@ -24,10 +24,14 @@ import android.os.Parcelable; import android.util.StringBuilderPrinter; +import com.mobilyzer.measurements.CronetHttpTask; +import com.mobilyzer.measurements.CronetHttpTask.CronetHttpDesc; import com.mobilyzer.measurements.DnsLookupTask; import com.mobilyzer.measurements.HttpTask; import com.mobilyzer.measurements.ParallelTask; import com.mobilyzer.measurements.PingTask; +import com.mobilyzer.measurements.QuicHttpTask; +import com.mobilyzer.measurements.QuicHttpTask.QuicDesc; import com.mobilyzer.measurements.RRCTask; import com.mobilyzer.measurements.SequentialTask; import com.mobilyzer.measurements.TCPThroughputTask; @@ -247,7 +251,11 @@ public String toString() { getPingResult(printer, values); } else if (type.equals(HttpTask.TYPE)) { getHttpResult(printer, values); - } else if (type.equals(DnsLookupTask.TYPE)) { + } else if (type.equals(CronetHttpTask.TYPE)) { + getCronetHttpResult(printer, values); + } else if (type.equals(QuicHttpTask.TYPE)) { + getQuicHttpResult(printer, values); + }else if (type.equals(DnsLookupTask.TYPE)) { getDnsResult(printer, values); } else if (type.equals(TracerouteTask.TYPE)) { getTracerouteResult(printer, values); @@ -333,6 +341,50 @@ private void getHttpResult(StringBuilderPrinter printer, HashMap } } + private void getQuicHttpResult(StringBuilderPrinter printer, HashMap values) { + QuicDesc desc = (QuicDesc) parameters; + printer.println("[QUIC]"); + printer.println("URL: " + desc.url); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + int headerLen = Integer.parseInt(values.get("headers_len")); + int bodyLen = Integer.parseInt(values.get("body_len")); + int time = Integer.parseInt(values.get("time_ms")); + printer.println(""); + printer.println("Downloaded " + (headerLen + bodyLen) + " bytes in " + time + " ms"); + printer.println("Bandwidth: " + (headerLen + bodyLen) * 8 / time + " Kbps"); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("Quic Http paused!"); + } else { + printer.println("Quic Http download failed, status code " + values.get("code")); + printer.println("Error: " + values.get("error")); + } + } + + private void getCronetHttpResult(StringBuilderPrinter printer, HashMap values) { + CronetHttpDesc desc = (CronetHttpDesc) parameters; + printer.println("[CronetHTTP]"); + printer.println("URL: " + desc.url); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + int headerLen = Integer.parseInt(values.get("headers_len")); + int bodyLen = Integer.parseInt(values.get("body_len")); + int time = Integer.parseInt(values.get("time_ms")); + printer.println(""); + printer.println("Downloaded " + (headerLen + bodyLen) + " bytes in " + time + " ms"); + printer.println("Bandwidth: " + (headerLen + bodyLen) * 8 / time + " Kbps"); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("CronetHttp paused!"); + } else { + printer.println("CronetHttp download failed, status code " + values.get("code")); + printer.println("Error: " + values.get("error")); + } + } + private void getDnsResult(StringBuilderPrinter printer, HashMap values) { DnsLookupDesc desc = (DnsLookupDesc) parameters; printer.println("[DNS Lookup]"); diff --git a/Mobilyzer/src/com/mobilyzer/MeasurementTask.java b/Mobilyzer/src/com/mobilyzer/MeasurementTask.java index eb90244..ca6e53b 100644 --- a/Mobilyzer/src/com/mobilyzer/MeasurementTask.java +++ b/Mobilyzer/src/com/mobilyzer/MeasurementTask.java @@ -8,10 +8,12 @@ import java.util.concurrent.Callable; import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.measurements.CronetHttpTask; import com.mobilyzer.measurements.DnsLookupTask; import com.mobilyzer.measurements.HttpTask; import com.mobilyzer.measurements.PageLoadTimeTask; import com.mobilyzer.measurements.PingTask; +import com.mobilyzer.measurements.QuicHttpTask; import com.mobilyzer.measurements.RRCTask; import com.mobilyzer.measurements.SequentialTask; import com.mobilyzer.measurements.TCPThroughputTask; @@ -48,6 +50,10 @@ public abstract class MeasurementTask measurementDescToType.put(PingTask.DESCRIPTOR, PingTask.TYPE); measurementTypes.put(HttpTask.TYPE, HttpTask.class); measurementDescToType.put(HttpTask.DESCRIPTOR, HttpTask.TYPE); + measurementTypes.put(CronetHttpTask.TYPE, CronetHttpTask.class); + measurementDescToType.put(CronetHttpTask.DESCRIPTOR, CronetHttpTask.TYPE); + measurementTypes.put(QuicHttpTask.TYPE, QuicHttpTask.class); + measurementDescToType.put(QuicHttpTask.DESCRIPTOR, QuicHttpTask.TYPE); measurementTypes.put(TracerouteTask.TYPE, TracerouteTask.class); measurementDescToType.put(TracerouteTask.DESCRIPTOR, TracerouteTask.TYPE); measurementTypes.put(DnsLookupTask.TYPE, DnsLookupTask.class); diff --git a/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java b/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java index 59ec096..dea2469 100644 --- a/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java +++ b/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java @@ -22,6 +22,7 @@ import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.measurements.QuicHttpTask; import com.mobilyzer.util.Logger; import com.mobilyzer.util.PhoneUtils; @@ -119,5 +120,4 @@ public MeasurementResult[] call() throws MeasurementError { return results; } - } diff --git a/Mobilyzer/src/com/mobilyzer/api/API.java b/Mobilyzer/src/com/mobilyzer/api/API.java index c68934a..4a180a4 100644 --- a/Mobilyzer/src/com/mobilyzer/api/API.java +++ b/Mobilyzer/src/com/mobilyzer/api/API.java @@ -14,23 +14,15 @@ */ package com.mobilyzer.api; -import java.security.InvalidParameterException; -import java.util.ArrayList; -import java.util.Date; -import java.util.LinkedList; -import java.util.Map; -import java.util.Queue; -import java.util.Set; - import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; -import android.os.Bundle; import com.mobilyzer.Config; import com.mobilyzer.MeasurementScheduler; @@ -38,28 +30,40 @@ import com.mobilyzer.MeasurementTask; import com.mobilyzer.UpdateIntent; import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.measurements.CronetHttpTask; +import com.mobilyzer.measurements.CronetHttpTask.CronetHttpDesc; import com.mobilyzer.measurements.DnsLookupTask; +import com.mobilyzer.measurements.DnsLookupTask.DnsLookupDesc; import com.mobilyzer.measurements.HttpTask; +import com.mobilyzer.measurements.HttpTask.HttpDesc; import com.mobilyzer.measurements.PageLoadTimeTask; import com.mobilyzer.measurements.PageLoadTimeTask.PageLoadTimeDesc; import com.mobilyzer.measurements.ParallelTask; -import com.mobilyzer.measurements.PingTask; -import com.mobilyzer.measurements.SequentialTask; -import com.mobilyzer.measurements.TCPThroughputTask; -import com.mobilyzer.measurements.TracerouteTask; -import com.mobilyzer.measurements.UDPBurstTask; -import com.mobilyzer.measurements.DnsLookupTask.DnsLookupDesc; -import com.mobilyzer.measurements.HttpTask.HttpDesc; import com.mobilyzer.measurements.ParallelTask.ParallelDesc; +import com.mobilyzer.measurements.PingTask; import com.mobilyzer.measurements.PingTask.PingDesc; +import com.mobilyzer.measurements.QuicHttpTask; +import com.mobilyzer.measurements.QuicHttpTask.QuicDesc; +import com.mobilyzer.measurements.SequentialTask; import com.mobilyzer.measurements.SequentialTask.SequentialDesc; +import com.mobilyzer.measurements.TCPThroughputTask; import com.mobilyzer.measurements.TCPThroughputTask.TCPThroughputDesc; +import com.mobilyzer.measurements.TracerouteTask; import com.mobilyzer.measurements.TracerouteTask.TracerouteDesc; +import com.mobilyzer.measurements.UDPBurstTask; import com.mobilyzer.measurements.UDPBurstTask.UDPBurstDesc; import com.mobilyzer.measurements.VideoQoETask; import com.mobilyzer.measurements.VideoQoETask.VideoQoEDesc; import com.mobilyzer.util.Logger; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + /** * @author jackjia,Hongyi Yao (hyyao@umich.edu) * The user API for Mobiperf library. @@ -68,9 +72,10 @@ * User: register BroadcastReceiver for userResultAction and serverResultAction */ public final class API { + public enum TaskType { DNSLOOKUP, HTTP, PING, TRACEROUTE, TCPTHROUGHPUT, UDPBURST, - PARALLEL, SEQUENTIAL, INVALID, PLT, VIDEOQOE + PARALLEL, SEQUENTIAL, INVALID, PLT, VIDEOQOE, QUICHTTP, CRONETHTTP } /** @@ -90,11 +95,12 @@ public enum TaskType { public final static int USER_PRIORITY = MeasurementTask.USER_PRIORITY; public final static int INVALID_PRIORITY = MeasurementTask.INVALID_PRIORITY; - + private Context applicationContext; private boolean isBound = false; private boolean isBindingToService = false; + Messenger mSchedulerMessenger = null; private String clientKey; @@ -282,6 +288,14 @@ public MeasurementTask createTask( TaskType taskType, Date startTime task = new HttpTask(new HttpDesc(clientKey, startTime, endTime , intervalSec, count, priority, contextIntervalSec, params)); break; + case QUICHTTP: + task = new QuicHttpTask(new QuicDesc(clientKey, startTime, endTime, + intervalSec, count, priority, contextIntervalSec, params)); + break; + case CRONETHTTP: + task = new CronetHttpTask(new CronetHttpDesc(clientKey, startTime, endTime, + intervalSec, count, priority, contextIntervalSec, params)); + break; case PING: task = new PingTask(new PingDesc(clientKey, startTime, endTime , intervalSec, count, priority, contextIntervalSec, params)); @@ -603,4 +617,5 @@ public static Set getMeasurementNames() { public static String getTypeForMeasurementName(String name) { return MeasurementTask.getTypeForMeasurementName(name); } + } diff --git a/Mobilyzer/src/com/mobilyzer/measurements/CronetHttpTask.java b/Mobilyzer/src/com/mobilyzer/measurements/CronetHttpTask.java new file mode 100644 index 0000000..4a15679 --- /dev/null +++ b/Mobilyzer/src/com/mobilyzer/measurements/CronetHttpTask.java @@ -0,0 +1,408 @@ +/* Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mobilyzer.measurements; + + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.Util; + +import org.chromium.net.CronetEngine; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.security.InvalidParameterException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A Callable class that performs download throughput test using HTTP via Google Cronet library + */ +public class CronetHttpTask extends MeasurementTask { + + + //////////////////////////////////////////////// + //// CONSTANTS + + // Type name for internal use + public static final String TYPE = "cronet-http"; + // Human readable name for the task + public static final String DESCRIPTOR = "CRONET-HTTP"; + // The maximum number of bytes we will read from requested URL. Set to 1Mb. + public static final long MAX_HTTP_RESPONSE_SIZE = 1024 * 1024; + // The size of the response body we will report to the service. + // If the response is larger than MAX_BODY_SIZE_TO_UPLOAD bytes, we will + // only report the first MAX_BODY_SIZE_TO_UPLOAD bytes of the body. + public static final int MAX_BODY_SIZE_TO_UPLOAD = 1024; + // The buffer size we use to read from the HTTP response stream + public static final int READ_BUFFER_SIZE = 1024; + // Not used by the HTTP protocol. Just in case we do not receive a status line from the response + public static final int DEFAULT_STATUS_CODE = 0; + + + //////////////////////////////////////////////// + //// FIELDS + + //Track data consumption for this task to avoid exceeding user's limit + private long dataConsumed; + private long duration; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public CronetHttpTask createFromParcel(Parcel in) { + return new CronetHttpTask(in); + } + + public CronetHttpTask[] newArray(int size) { + return new CronetHttpTask[size]; + } + }; + + //////////////////////////////////////////////// + //// CONSTRUCTORS + + public CronetHttpTask(MeasurementDesc desc) { + super(new CronetHttpDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + this.duration = Config.DEFAULT_HTTP_TASK_DURATION; + this.dataConsumed = 0; + } + + protected CronetHttpTask(Parcel in) { + super(in); + duration = in.readLong(); + dataConsumed = in.readLong(); + } + + + //////////////////////////////////////////////// + //// INNER CLASS + + /** + * The description of a HTTP measurement via Cronet + */ + public static class CronetHttpDesc extends MeasurementDesc { + public String url; + private String method; + private String headers; + private String body; + + public CronetHttpDesc(String key, Date startTime, Date endTime, + double intervalSec, long count, long priority, int contextIntervalSec, + Map params) throws InvalidParameterException { + super(CronetHttpTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + if (params == null) { + throw new InvalidParameterException("Parameters for the measurement are not provided"); + } + initializeParams(params); + } + + @Override + protected void initializeParams(Map params) { + + String urlParam = params.get("url"); + if (urlParam == null || urlParam.isEmpty()) { + throw new InvalidParameterException("URL for cronet-http task is null"); + } + + this.url = urlParam; + if (!this.url.startsWith("http://") && !this.url.startsWith("https://")) { + this.url = "http://" + this.url; + } + + String methodParam = params.get("method"); + if (methodParam == null || methodParam.isEmpty()) { + this.method = "GET"; + } + else { + this.method = methodParam.toUpperCase(); + } + + this.headers = params.get("headers"); + this.body = params.get("body"); + } + + @Override + public String getType() { + return CronetHttpTask.TYPE; + } + + + protected CronetHttpDesc(Parcel in) { + super(in); + url = in.readString(); + method = in.readString(); + headers = in.readString(); + body = in.readString(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public CronetHttpDesc createFromParcel(Parcel in) { + return new CronetHttpDesc(in); + } + + public CronetHttpDesc[] newArray(int size) { + return new CronetHttpDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(url); + dest.writeString(method); + dest.writeString(headers); + dest.writeString(body); + } + } + + //////////////////////////////////////////////// + //// METHODS + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + dest.writeLong(dataConsumed); + } + + + /** + * Returns a copy of the CronetHttpTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + CronetHttpDesc newDesc = new CronetHttpDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, + desc.parameters); + return new CronetHttpTask(newDesc); + } + + /** + * Runs the HTTP measurement task. Will acquire power lock to ensure wifi + * is not turned off + */ + @Override + public MeasurementResult[] call() throws MeasurementError { + TaskProgress taskProgress = TaskProgress.FAILED; + int statusCode = DEFAULT_STATUS_CODE; + ByteBuffer body = ByteBuffer.allocate(CronetHttpTask.MAX_BODY_SIZE_TO_UPLOAD); + String headers = ""; + byte[] readBuffer = new byte[CronetHttpTask.READ_BUFFER_SIZE]; + int totalBodyLen = 0; + + long currentRxTx = Util.getCurrentRxTxBytes(); + + CronetHttpDesc task = (CronetHttpDesc) this.measurementDesc; + + HttpURLConnection urlConnection = null; + + try { + URL url = new URL(task.url); + CronetEngine.Builder cronetBuilder = + new CronetEngine.Builder(PhoneUtils.getGlobalContext()); + CronetEngine cronetEngine = cronetBuilder.build(); + + urlConnection = (HttpURLConnection) cronetEngine.openConnection(url); + urlConnection.setRequestMethod(task.method); + boolean doOutput = false; + + /* Add request headers */ + if (task.headers != null && task.headers.trim().length() > 0) { + doOutput = true; + for (String headerLine : task.headers.split("\r\n")) { + String tokens[] = headerLine.split(":"); + if (tokens.length == 2) { + urlConnection.setRequestProperty(tokens[0], tokens[1]); + } else { + throw new MeasurementError("Incorrect header line: " + headerLine); + } + } + } + + /* Do not follow redirects */ + urlConnection.setInstanceFollowRedirects(false); + /* If we do the output */ + if (!(task.body == null || task.body.isEmpty())) { + doOutput = true; + } + urlConnection.setDoOutput(doOutput); + + /* Start measuring */ + long startTime = System.currentTimeMillis(); + + + /* Write body */ + if (!(task.body == null || task.body.isEmpty())) { + OutputStream outputStream = urlConnection.getOutputStream(); + outputStream.write(task.body.getBytes()); + outputStream.flush(); + outputStream.close(); + } + + /* Get response code */ + statusCode = urlConnection.getResponseCode(); + + /* Get response body */ + if (statusCode == HttpURLConnection.HTTP_OK) { + int readLen; + InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream()); + while ((readLen = inputStream.read(readBuffer)) > 0 + && totalBodyLen <= CronetHttpTask.MAX_HTTP_RESPONSE_SIZE) { + totalBodyLen += readLen; + // Fill in the body to report up to MAX_BODY_SIZE + if (body.remaining() > 0) { + int putLen = body.remaining() < readLen ? body.remaining() : readLen; + body.put(readBuffer, 0, putLen); + } + } + + } + + /* Stop measuring */ + duration = System.currentTimeMillis() - startTime; + + /* Set task result */ + taskProgress = statusCode == HttpURLConnection.HTTP_OK ? + TaskProgress.COMPLETED : TaskProgress.FAILED; + + /* Process headers */ + Map> responseHeaders = urlConnection.getHeaderFields(); + if (!responseHeaders.isEmpty()) { + headers = ""; + for (String headerKey : responseHeaders.keySet()) { + List headerValues = responseHeaders.get(headerKey); + headers += headerKey + ": "; + boolean first = true; + for (String headerValue : headerValues) { + if (!first) { + headers += ", "; + } + headers += headerValue; + first = false; + } + headers += "\r\n"; + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + + + dataConsumed += (Util.getCurrentRxTxBytes() - currentRxTx); + + /* Prepare result */ + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + CronetHttpTask.TYPE, System.currentTimeMillis() * 1000, + taskProgress, this.measurementDesc); + + result.addResult("code", statusCode); + + if (taskProgress == TaskProgress.COMPLETED) { + result.addResult("time_ms", duration); + result.addResult("headers_len", headers.length()); + result.addResult("body_len", totalBodyLen); + result.addResult("headers", headers); + result.addResult("body", Base64.encodeToString(body.array(), Base64.DEFAULT)); + } + + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray = new MeasurementResult[1]; + mrArray[0] = result; + return mrArray; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return CronetHttpDesc.class; + } + + @Override + public String getType() { + return CronetHttpTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public String toString() { + CronetHttpDesc desc = (CronetHttpDesc) measurementDesc; + return "[CRONETHTTP " + desc.method + "]\n Target: " + desc.url + + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + + desc.startTime; + } + + @Override + public boolean stop() { + return false; + } + + @Override + public long getDuration() { + return this.duration; + } + + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + + /** + * Data used so far by the task. + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } +} diff --git a/Mobilyzer/src/com/mobilyzer/measurements/QuicHttpTask.java b/Mobilyzer/src/com/mobilyzer/measurements/QuicHttpTask.java new file mode 100644 index 0000000..fb46ecd --- /dev/null +++ b/Mobilyzer/src/com/mobilyzer/measurements/QuicHttpTask.java @@ -0,0 +1,404 @@ + +package com.mobilyzer.measurements; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.Util; + +import org.chromium.net.CronetEngine; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.security.InvalidParameterException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A Callable class that performs download throughput test using QUIC via Google Cronet library + */ +public class QuicHttpTask extends MeasurementTask { + + //////////////////////////////////////////////// + //// CONSTANTS + + // Type name for internal use + public static final String TYPE = "quic-http"; + // Human readable name for the task + public static final String DESCRIPTOR = "QUIC-HTTP"; + // The maximum number of bytes we will read from requested URL. Set to 1Mb. + public static final long MAX_QUIC_HTTP_RESPONSE_SIZE = 1024 * 1024; + // The size of the response body we will report to the service. + // If the response is larger than MAX_BODY_SIZE_TO_UPLOAD bytes, we will + // only report the first MAX_BODY_SIZE_TO_UPLOAD bytes of the body. + public static final int MAX_BODY_SIZE_TO_UPLOAD = 1024; + // The buffer size we use to read from the HTTP response stream + public static final int READ_BUFFER_SIZE = 1024; + // Not used by the HTTP protocol. Just in case we do not receive a status line from the response + public static final int DEFAULT_STATUS_CODE = 0; + // Option for the Cronet builder to force QUIC on host + port + private static final String FORCE_QUIC_OPTION = "origin_to_force_quic_on"; + + + + //////////////////////////////////////////////// + //// FIELDS + + //Track data consumption for this task to avoid exceeding user's limit + private long dataConsumed; + private long duration; + + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public QuicHttpTask createFromParcel(Parcel in) { + return new QuicHttpTask(in); + } + + public QuicHttpTask[] newArray(int size) { + return new QuicHttpTask[size]; + } + }; + + //////////////////////////////////////////////// + //// CONSTRUCTORS + + public QuicHttpTask(MeasurementDesc desc) { + super(new QuicDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + this.duration = Config.DEFAULT_HTTP_TASK_DURATION; + this.dataConsumed = 0; + } + + protected QuicHttpTask(Parcel in) { + super(in); + duration = in.readLong(); + dataConsumed = in.readLong(); + } + + + //////////////////////////////////////////////// + //// INNER CLASS + + /** + * The description of a Quic measurement + */ + public static class QuicDesc extends MeasurementDesc { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public QuicDesc createFromParcel(Parcel in) { + return new QuicDesc(in); + } + + public QuicDesc[] newArray(int size) { + return new QuicDesc[size]; + } + }; + + public String url; + private String method; + private String headers; + private String body; + + public QuicDesc(String key, Date startTime, Date endTime, + double intervalSec, long count, long priority, int contextIntervalSec, + Map params) throws InvalidParameterException { + super(QuicHttpTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + if (params == null) { + throw new InvalidParameterException("Parameters for the measurement are not provided"); + } + initializeParams(params); + } + + @Override + protected void initializeParams(Map params) { + String urlParam = params.get("url"); + if (urlParam == null || urlParam.isEmpty()) { + throw new InvalidParameterException("URL for quic http task is null"); + } + + url = urlParam.startsWith("https://") ? urlParam : "https://" + urlParam; + + headers = params.get("headers"); + body = params.get("body"); + method = (body == null || body.isEmpty()) ? "GET" : "POST"; + } + + @Override + public String getType() { + return QuicHttpTask.TYPE; + } + + protected QuicDesc(Parcel in) { + super(in); + url = in.readString(); + method = in.readString(); + headers = in.readString(); + body = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(url); + dest.writeString(method); + dest.writeString(headers); + dest.writeString(body); + } + } + + + //////////////////////////////////////////////// + //// METHODS + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + dest.writeLong(dataConsumed); + } + + + @Override + public MeasurementTask clone() { + QuicDesc desc = (QuicDesc) this.measurementDesc; + QuicDesc newDesc = new QuicDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, + desc.parameters); + return new QuicHttpTask(newDesc); + } + + /** + * Runs the Quic measurement task. Will acquire power lock to ensure wifi + * is not turned off + */ + @Override + public MeasurementResult[] call() throws MeasurementError { + TaskProgress taskProgress = TaskProgress.FAILED; + int statusCode = DEFAULT_STATUS_CODE; + ByteBuffer body = ByteBuffer.allocate(QuicHttpTask.MAX_BODY_SIZE_TO_UPLOAD); + String headers = ""; + byte[] readBuffer = new byte[QuicHttpTask.READ_BUFFER_SIZE]; + int totalBodyLen = 0; + + long currentRxTx = Util.getCurrentRxTxBytes(); + + QuicDesc task = (QuicDesc) this.measurementDesc; + + HttpURLConnection urlConnection = null; + + try { + URL url = new URL(task.url); + + /* Build new Cronet engine */ + CronetEngine.Builder cronetBuilder = + new CronetEngine.Builder(PhoneUtils.getGlobalContext()); + cronetBuilder.enableQUIC(true); + int quicPort = url.getPort() == -1 ? 443 : url.getPort(); + cronetBuilder.addQuicHint(url.getHost(), quicPort, quicPort); + + + JSONObject quicParams = new JSONObject().put(FORCE_QUIC_OPTION, url.getHost() + ":" + quicPort); + JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams); + cronetBuilder.setExperimentalOptions(experimentalOptions.toString()); + CronetEngine cronetEngine = cronetBuilder.build(); + + urlConnection = (HttpURLConnection) cronetEngine.openConnection(url); + urlConnection.setRequestMethod(task.method); + + boolean doOutput = false; + + /* Add request headers */ + if (task.headers != null && task.headers.trim().length() > 0) { + doOutput = true; + for (String headerLine : task.headers.split("\r\n")) { + String tokens[] = headerLine.split(":"); + if (tokens.length == 2) { + urlConnection.setRequestProperty(tokens[0], tokens[1]); + } else { + throw new MeasurementError("Incorrect header line: " + headerLine); + } + } + } + + /* Do not follow redirects */ + urlConnection.setInstanceFollowRedirects(false); + /* If we do the output */ + if (!(task.body == null || task.body.isEmpty())) { + doOutput = true; + } + urlConnection.setDoOutput(doOutput); + + /* Start measuring */ + long startTime = System.currentTimeMillis(); + + + /* Write body */ + if (!(task.body == null || task.body.isEmpty())) { + OutputStream outputStream = urlConnection.getOutputStream(); + outputStream.write(task.body.getBytes()); + outputStream.flush(); + outputStream.close(); + } + + /* Get response code */ + statusCode = urlConnection.getResponseCode(); + + /* Get response body */ + if (statusCode == HttpURLConnection.HTTP_OK) { + int readLen; + InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream()); + while ((readLen = inputStream.read(readBuffer)) > 0 + && totalBodyLen <= QuicHttpTask.MAX_QUIC_HTTP_RESPONSE_SIZE) { + totalBodyLen += readLen; + // Fill in the body to report up to MAX_BODY_SIZE + if (body.remaining() > 0) { + int putLen = body.remaining() < readLen ? body.remaining() : readLen; + body.put(readBuffer, 0, putLen); + } + } + + } + + /* Stop measuring */ + duration = System.currentTimeMillis() - startTime; + + /* Set task result */ + taskProgress = statusCode == HttpURLConnection.HTTP_OK ? + TaskProgress.COMPLETED : TaskProgress.FAILED; + + /* Process headers */ + Map> responseHeaders = urlConnection.getHeaderFields(); + if (!responseHeaders.isEmpty()) { + headers = ""; + for (String headerKey : responseHeaders.keySet()) { + List headerValues = responseHeaders.get(headerKey); + headers += headerKey + ": "; + boolean first = true; + for (String headerValue : headerValues) { + if (!first) { + headers += ", "; + } + headers += headerValue; + first = false; + } + headers += "\r\n"; + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + catch (JSONException e) { + e.printStackTrace(); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + + } + + + dataConsumed += (Util.getCurrentRxTxBytes() - currentRxTx); + + /* Prepare result */ + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + QuicHttpTask.TYPE, System.currentTimeMillis() * 1000, + taskProgress, this.measurementDesc); + + result.addResult("code", statusCode); + + if (taskProgress == TaskProgress.COMPLETED) { + result.addResult("time_ms", duration); + result.addResult("headers_len", headers.length()); + result.addResult("body_len", totalBodyLen); + result.addResult("headers", headers); + result.addResult("body", Base64.encodeToString(body.array(), Base64.DEFAULT)); + } + + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray = new MeasurementResult[1]; + mrArray[0] = result; + return mrArray; + + } + + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return QuicDesc.class; + } + + @Override + public String getType() { + return QuicHttpTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public String toString() { + QuicDesc desc = (QuicDesc) measurementDesc; + return "[QUICHTTP " + desc.method + "]\n Target: " + desc.url + + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + + desc.startTime; + } + + @Override + public boolean stop() { + return false; + } + + @Override + public long getDuration() { + return this.duration; + } + + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + + /** + * Data used so far by the task. + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } + +} \ No newline at end of file diff --git a/README.md b/README.md index e98d109..93633d1 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,46 @@ Mobilyzer is a network measurment platform, developed for Android applications. ##Important: How to Integrate Mobilyzer for Mobile Apps -You need Eclipse or ADT bundle to integrate Mobilyzer into your Android apps. +**Note** In the provided version of Cronet library certificate verification is disabled for QUIC and is set up to ignore certificate errors HTTPS, thus communication using CronetHttpTask and QuicHttpTask is not secure. + +You need Android Studio to integrate Mobilyzer into your Android apps. + +**Cloning source code:** +1. Create a **mobilibs** directory next to your app directory. +2. git clone the ExoPlayer and Mobilyzer repositories to it. +3. Add the following lines to the **settings.gradle** of your app: + +  `include ':mobilibs:Mobilyzer:Mobilyzer'` +  `include ':mobilibs:ExoPlayer:library'` + +**Configuring app project:** +
    +
  1. Create **quiclibs** folder next to **src** folder of your app, copy jars from the **quiclibs** folder in Mobilyzer to it and add the following to the dependencies of your app’s **build.gradle** file
  2. + +  `dependencies {` +    `apk files('quiclibs/base_java.jar')` +    `apk files('quiclibs/jsr_305_javalib.jar')` +    `apk files('quiclibs/net_java.jar')` +    `apk files('quiclibs/url_java.jar')` +  `}` + +
  3. Create a **jniLibs** folder under **src/main** of your project and copy the contents of the **jniLibs** folder in Mobilyzer to it, add reference to it to the `android {}` section your app’s **build.gradle** file
  4. + +  `sourceSets {` +    ` main {` +      `jniLibs.srcDirs = ['src/main/jniLibs'] ` +    `}` +  `}` + +
  5. Add Mobilyzer library project as a compile dependency in your app's **build.gradle** file
  6. + +  `dependencies {` +    `compile project(':mobilibs:Mobilyzer:Mobilyzer')` + +    `apk files('quiclibs/base_java.jar')` +    `apk files('quiclibs/jsr_305_javalib.jar')` +    `apk files('quiclibs/net_java.jar')` +    `apk files('quiclibs/url_java.jar')` +  `}` + -1. Clone this project and import it into your Eclipse workspace. -2. Add a reference to the Mobilyzer project library project to your app with this guide (http://developer.android.com/tools/projects/projects-eclipse.html#ReferencingLibraryProject). -3. You must add "manifestmerger.enabled=true" to your application's project.properties. Forget to enable manifestmerger will cause no scheduler error. -4. Add google play service library project (http://developer.android.com/google/play-services/setup.html). -5. Add all the jar files in libs folder to your build path.