/* 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.net.http.AndroidHttpClient;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import com.mobilyzer.Config;
import com.mobilyzer.MeasurementDesc;
import com.mobilyzer.MeasurementResult;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.MeasurementResult.TaskProgress;
import com.mobilyzer.exceptions.MeasurementError;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.security.InvalidParameterException;
import java.util.Date;
import java.util.Map;
/**
* A Callable class that performs download throughput test using HTTP get
*/
public class HttpTask extends MeasurementTask {
// Type name for internal use
public static final String TYPE = "http";
// Human readable name for the task
public static final String DESCRIPTOR = "HTTP";
/* TODO(Wenjie): Depending on state machine configuration of cell tower's radio,
* the size to find the 'real' bandwidth of the phone may be network dependent.
*/
// 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;
//Track data consumption for this task to avoid exceeding user's limit
private long dataConsumed;
private AndroidHttpClient httpClient = null;
private long duration;
public HttpTask(MeasurementDesc desc) {
super(new HttpDesc(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 HttpTask(Parcel in) {
super(in);
duration = in.readLong();
dataConsumed = in.readLong();
}
public static final Parcelable.Creator<HttpTask> CREATOR =
new Parcelable.Creator<HttpTask>() {
public HttpTask createFromParcel(Parcel in) {
return new HttpTask(in);
}
public HttpTask[] newArray(int size) {
return new HttpTask[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(duration);
dest.writeLong(dataConsumed);
}
/**
* The description of a HTTP measurement
*/
public static class HttpDesc extends MeasurementDesc {
public String url;
private String method;
private String headers;
private String body;
public HttpDesc(String key, Date startTime, Date endTime,
double intervalSec, long count, long priority, int contextIntervalSec,
Map<String, String> params) throws InvalidParameterException {
super(HttpTask.TYPE, key, startTime, endTime, intervalSec, count,
priority,contextIntervalSec, params);
initializeParams(params);
if (this.url == null || this.url.length() == 0) {
throw new InvalidParameterException("URL for http task is null");
}
}
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
this.url = params.get("url");
if (!this.url.startsWith("http://") && !this.url.startsWith("https://")) {
this.url = "http://" + this.url;
}
this.method = params.get("method");
if (this.method == null || this.method.isEmpty()) {
this.method = "get";
}
this.headers = params.get("headers");
this.body = params.get("body");
}
@Override
public String getType() {
return HttpTask.TYPE;
}
protected HttpDesc(Parcel in) {
super(in);
url = in.readString();
method = in.readString();
headers = in.readString();
body = in.readString();
}
public static final Parcelable.Creator<HttpDesc> CREATOR =
new Parcelable.Creator<HttpDesc>() {
public HttpDesc createFromParcel(Parcel in) {
return new HttpDesc(in);
}
public HttpDesc[] newArray(int size) {
return new HttpDesc[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);
}
}
/**
* Returns a copy of the HttpTask
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
HttpDesc newDesc = new HttpDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec,
desc.parameters);
return new HttpTask(newDesc);
}
/** Runs the HTTP measurement task. Will acquire power lock to ensure wifi
* is not turned off */
@Override
public MeasurementResult[] call() throws MeasurementError {
int statusCode = HttpTask.DEFAULT_STATUS_CODE;
long duration = 0;
long originalHeadersLen = 0;
long originalBodyLen;
String headers = null;
ByteBuffer body = ByteBuffer.allocate(HttpTask.MAX_BODY_SIZE_TO_UPLOAD);
// boolean success = false;
TaskProgress taskProgress=TaskProgress.FAILED;
String errorMsg = "";
InputStream inputStream = null;
long currentRxTx=Util.getCurrentRxTxBytes();
try {
// set the download URL, a URL that points to a file on the Internet
// this is the file to be downloaded
HttpDesc task = (HttpDesc) this.measurementDesc;
String urlStr = task.url;
// TODO(Wenjie): Need to set timeout for the HTTP methods
httpClient = AndroidHttpClient.newInstance(Util.prepareUserAgent());
HttpRequestBase request = null;
if (task.method.compareToIgnoreCase("head") == 0) {
request = new HttpHead(urlStr);
} else if (task.method.compareToIgnoreCase("get") == 0) {
request = new HttpGet(urlStr);
} else if (task.method.compareToIgnoreCase("post") == 0) {
request = new HttpPost(urlStr);
HttpPost postRequest = (HttpPost) request;
postRequest.setEntity(new StringEntity(task.body));
} else {
// Use GET by default
request = new HttpGet(urlStr);
}
if (task.headers != null && task.headers.trim().length() > 0) {
for (String headerLine : task.headers.split("\r\n")) {
String tokens[] = headerLine.split(":");
if (tokens.length == 2) {
request.addHeader(tokens[0], tokens[1]);
} else {
throw new MeasurementError("Incorrect header line: " + headerLine);
}
}
}
byte[] readBuffer = new byte[HttpTask.READ_BUFFER_SIZE];
int readLen;
int totalBodyLen = 0;
long startTime = System.currentTimeMillis();
HttpResponse response = httpClient.execute(request);
/* TODO(Wenjie): HttpClient does not automatically handle the following codes
* 301 Moved Permanently. HttpStatus.SC_MOVED_PERMANENTLY
* 302 Moved Temporarily. HttpStatus.SC_MOVED_TEMPORARILY
* 303 See Other. HttpStatus.SC_SEE_OTHER
* 307 Temporary Redirect. HttpStatus.SC_TEMPORARY_REDIRECT
*
* We may want to fetch instead from the redirected page.
*/
StatusLine statusLine = response.getStatusLine();
if (statusLine != null) {
statusCode = statusLine.getStatusCode();
if(statusCode == 200){
taskProgress=TaskProgress.COMPLETED;
}
else{
taskProgress=TaskProgress.FAILED;
}
}
/* For HttpClient to work properly, we still want to consume the entire
* response even if the status code is not 200
*/
HttpEntity responseEntity = response.getEntity();
originalBodyLen = responseEntity.getContentLength();
long expectedResponseLen = HttpTask.MAX_HTTP_RESPONSE_SIZE;
// getContentLength() returns negative number if body length is unknown
if (originalBodyLen > 0) {
expectedResponseLen = originalBodyLen;
}
if (responseEntity != null) {
inputStream = responseEntity.getContent();
while ((readLen = inputStream.read(readBuffer)) > 0
&& totalBodyLen <= HttpTask.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);
}
}
duration = System.currentTimeMillis() - startTime;//TODO check this
}
Header[] responseHeaders = response.getAllHeaders();
if (responseHeaders != null) {
headers = "";
for (Header hdr : responseHeaders) {
/*
* TODO(Wenjie): There can be preceding and trailing white spaces in
* each header field. I cannot find internal methods that return the
* number of bytes in a header. The solution here assumes the encoding
* is one byte per character.
*/
originalHeadersLen += hdr.toString().length();
headers += hdr.toString() + "\r\n";
}
}
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
MeasurementResult result = new MeasurementResult(
phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(this.getKey()),
HttpTask.TYPE, System.currentTimeMillis() * 1000,
taskProgress, this.measurementDesc);
result.addResult("code", statusCode);
dataConsumed+=(Util.getCurrentRxTxBytes()-currentRxTx);
if (taskProgress==TaskProgress.COMPLETED) {
result.addResult("time_ms", duration);
result.addResult("headers_len", originalHeadersLen);
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;
} catch (MalformedURLException e) {
errorMsg += e.getMessage() + "\n";
Logger.e(e.getMessage());
} catch (IOException e) {
errorMsg += e.getMessage() + "\n";
Logger.e(e.getMessage());
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Logger.e("Fails to close the input stream from the HTTP response");
}
}
if (httpClient != null) {
httpClient.close();
}
}
throw new MeasurementError("Cannot get result from HTTP measurement because "
+ errorMsg);
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return HttpDesc.class;
}
@Override
public String getType() {
return HttpTask.TYPE;
}
@Override
public String getDescriptor() {
return DESCRIPTOR;
}
@Override
public String toString() {
HttpDesc desc = (HttpDesc) measurementDesc;
return "[HTTP " + 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;
}
}