diff --git a/Mobilyzer/src/com/mobilyzer/MeasurementScheduler.java b/Mobilyzer/src/com/mobilyzer/MeasurementScheduler.java index 948c0ab..53b3b56 100644 --- a/Mobilyzer/src/com/mobilyzer/MeasurementScheduler.java +++ b/Mobilyzer/src/com/mobilyzer/MeasurementScheduler.java @@ -185,6 +185,9 @@ public void onCreate() { filter.addAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); filter.addAction(UpdateIntent.GCM_MEASUREMENT_ACTION); filter.addAction(UpdateIntent.PLT_MEASUREMENT_ACTION); + //added by Clarence + filter.addAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + broadcastReceiver = new BroadcastReceiver() { @@ -323,7 +326,20 @@ public void onReceive(Context context, Intent intent) { } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( Config.TASK_RESUMED)) { tasksStatus.put(taskid, TaskStatus.RUNNING); - } + } //added by Clarence + } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION)){ + String IM_taskKey = intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD); + String IM_taskid = intent.getStringExtra(UpdateIntent.TASKID_PAYLOAD); + int IM_priority = + intent.getIntExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.INVALID_PRIORITY); + Parcelable[] IM_Results = intent.getParcelableArrayExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD); + if (IM_Results != null && IM_Results.length!=0) { + sendResultToClient(IM_Results, IM_priority, IM_taskKey, IM_taskid); + Logger.i("Sending Intermediate results to client..."); + System.out.println("receive the intermediate results"); + } + } else if (intent.getAction().equals(UpdateIntent.CHECKIN_ACTION) || intent.getAction().equals(UpdateIntent.CHECKIN_RETRY_ACTION)) { Logger.d("Checkin intent received"); diff --git a/Mobilyzer/src/com/mobilyzer/MeasurementTask.java b/Mobilyzer/src/com/mobilyzer/MeasurementTask.java index eb90244..8bc0318 100644 --- a/Mobilyzer/src/com/mobilyzer/MeasurementTask.java +++ b/Mobilyzer/src/com/mobilyzer/MeasurementTask.java @@ -29,6 +29,8 @@ public abstract class MeasurementTask Parcelable { protected MeasurementDesc measurementDesc; protected String taskId; + + private MeasurementScheduler scheduler; // added by Clarence public static final int USER_PRIORITY = Integer.MIN_VALUE; @@ -114,6 +116,15 @@ public String getKey() { public void setKey(String key) { this.measurementDesc.key = key; } + + // added by Clarence, pass scheduler to every specific task + public void setScheduler(MeasurementScheduler scheduler) { + this.scheduler = scheduler; + } + + public MeasurementScheduler getScheduler() { + return this.scheduler; + } public MeasurementDesc getDescription() { diff --git a/Mobilyzer/src/com/mobilyzer/ServerMeasurementTask.java b/Mobilyzer/src/com/mobilyzer/ServerMeasurementTask.java index ce5a231..250cf79 100644 --- a/Mobilyzer/src/com/mobilyzer/ServerMeasurementTask.java +++ b/Mobilyzer/src/com/mobilyzer/ServerMeasurementTask.java @@ -38,6 +38,7 @@ public class ServerMeasurementTask implements Callable { public ServerMeasurementTask(MeasurementTask task, MeasurementScheduler scheduler, ResourceCapManager manager) { realTask = task; + realTask.setScheduler(null); //added by Clarence this.scheduler = scheduler; this.contextCollector = new ContextCollector(); this.rManager = manager; diff --git a/Mobilyzer/src/com/mobilyzer/UpdateIntent.java b/Mobilyzer/src/com/mobilyzer/UpdateIntent.java index e16b5fa..f6be044 100644 --- a/Mobilyzer/src/com/mobilyzer/UpdateIntent.java +++ b/Mobilyzer/src/com/mobilyzer/UpdateIntent.java @@ -44,6 +44,8 @@ public class UpdateIntent extends Intent { public static final String VIDEO_TASK_PAYLOAD_REBUFFER_TIME = "VIDEO_TASK_PAYLOAD_REBUFFER_TIME"; public static final String VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME = "VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME"; public static final String VIDEO_TASK_PAYLOAD_BYTE_USED = "VIDEO_TASK_PAYLOAD_BYTE_USED"; + //added by Clarence + public static final String INTERMEDIATE_RESULT_PAYLOAD = "INTERMEDIATE_RESULT_PAYLOAD"; // Different types of actions that this intent can represent: @@ -81,6 +83,11 @@ public class UpdateIntent extends Intent { PACKAGE_PREFIX + ".DATA_USAGE_ACTION"; public static final String AUTH_ACCOUNT_ACTION = PACKAGE_PREFIX + ".AUTH_ACCOUNT_ACTION"; + + + // added by Clarence + public static final String MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION = + APP_PREFIX + ".MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION"; /** * Creates an intent of the specified action with an optional message diff --git a/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java b/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java index 59ec096..f371214 100644 --- a/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java +++ b/Mobilyzer/src/com/mobilyzer/UserMeasurementTask.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.concurrent.Callable; +import android.content.Context; import android.content.Intent; import com.mobilyzer.MeasurementResult.TaskProgress; @@ -33,6 +34,7 @@ public class UserMeasurementTask implements Callable { public UserMeasurementTask(MeasurementTask task, MeasurementScheduler scheduler) { realTask = task; + realTask.setScheduler(scheduler); //added by Clarence this.scheduler = scheduler; this.contextCollector= new ContextCollector(); } @@ -48,7 +50,10 @@ private void broadcastMeasurementStart() { intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_STARTED); intent.putExtra(UpdateIntent.TASKID_PAYLOAD, realTask.getTaskId()); intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, realTask.getKey()); +// Context context = scheduler.getApplicationContext(); +// context.sendBroadcast(intent); scheduler.sendBroadcast(intent); + } /** @@ -96,9 +101,12 @@ public MeasurementResult[] call() throws MeasurementError { ArrayList> contextResults = contextCollector.stopCollector(); for (MeasurementResult r: results){ + String testIntermediate1 = r.toString(); r.addContextResults(contextResults); r.getDeviceProperty().dnResolvability=contextCollector.dnsConnectivity; r.getDeviceProperty().ipConnectivity=contextCollector.ipConnectivity; + String testIntermediate2 = r.toString(); + } } catch (MeasurementError e) { Logger.e("User measurement " + realTask.getDescriptor() + " has failed"); diff --git a/Mobilyzer/src/com/mobilyzer/measurements/ParallelTask.java b/Mobilyzer/src/com/mobilyzer/measurements/ParallelTask.java index 9cf9c36..46c888a 100644 --- a/Mobilyzer/src/com/mobilyzer/measurements/ParallelTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/ParallelTask.java @@ -165,6 +165,9 @@ public String getDescriptor() { @Override public MeasurementResult[] call() throws MeasurementError { + for (MeasurementTask task: this.tasks){ + task.setScheduler(this.getScheduler()); + } //added by Clarence long timeout=duration; executor=Executors.newFixedThreadPool(this.tasks.size()); diff --git a/Mobilyzer/src/com/mobilyzer/measurements/PingTask.java b/Mobilyzer/src/com/mobilyzer/measurements/PingTask.java index 6d26ee3..13868e5 100755 --- a/Mobilyzer/src/com/mobilyzer/measurements/PingTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/PingTask.java @@ -15,6 +15,7 @@ package com.mobilyzer.measurements; +import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; @@ -23,7 +24,9 @@ import com.mobilyzer.Config; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementScheduler; import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; @@ -71,6 +74,32 @@ public class PingTask extends MeasurementTask{ //Track data consumption for this task to avoid exceeding user's limit private long dataConsumed; + + private MeasurementScheduler scheduler = null; // added by Clarence + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, MeasurementScheduler scheduler) { + this.scheduler = scheduler; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.scheduler.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } + /** * Encode ping specific parameters, along with common parameters inherited from MeasurmentDesc * @author wenjiezeng@google.com (Steve Zeng) @@ -356,6 +385,11 @@ private MeasurementResult executePingCmdTask(int ipByteLen) PingDesc pingTask = (PingDesc) this.measurementDesc; String errorMsg = ""; MeasurementResult measurementResult = null; + + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; + // TODO(Wenjie): Add a exhaustive list of ping locations for different // Android phones pingTask.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen); @@ -384,11 +418,13 @@ private MeasurementResult executePingCmdTask(int ipByteLen) ArrayList receivedIcmpSeq = new ArrayList(); double packetLoss = Double.MIN_VALUE; int packetsSent = Config.PING_COUNT_PER_MEASUREMENT; + // Process each line of the ping output and store the rrt in array rrts. while ((line = br.readLine()) != null) { // Ping prints a number of 'param=value' pairs, among which we only need // the 'time=rrt_val' pair String[] extractedValues = Util.extractInfoFromPingOutput(line); + if (extractedValues != null) { int curIcmpSeq = Integer.parseInt(extractedValues[0]); double rrtVal = Double.parseDouble(extractedValues[1]); @@ -409,6 +445,18 @@ private MeasurementResult executePingCmdTask(int ipByteLen) int packetsReceived = packetLossInfo[1]; packetLoss = 1 - ((double) packetsReceived / (double) packetsSent); } + this.scheduler = this.getScheduler(); + if ( this.scheduler != null ){ + if (rrts.size() >= 2 && (rrts.size() < Config.PING_COUNT_PER_MEASUREMENT) && (extractedValues != null) ){ + IM_measurementResult = constructResult(rrts,packetLoss, + rrts.size(),PING_METHOD_CMD); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.scheduler); + + + } + } + //IM_packetsSent++; Logger.i(line); } @@ -452,6 +500,10 @@ private MeasurementResult executeJavaPingTask() throws MeasurementError { ArrayList rrts = new ArrayList(); String errorMsg = ""; MeasurementResult result = null; + double packetLoss = Double.MIN_VALUE; //added by Clarence + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; try { int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec / @@ -466,11 +518,28 @@ private MeasurementResult executeJavaPingTask() throws MeasurementError { if (status) { totalPingDelay += rrtVal; rrts.add((double) rrtVal); + packetLoss = 1 - + ((double) rrts.size() / (i+1)); + dataConsumed += pingTask.packetSizeByte * (i+1) * 2; + this.scheduler = this.getScheduler(); + if ( this.scheduler != null){ + IM_measurementResult = constructResult(rrts,packetLoss, + (i+1),PING_METHOD_JAVA); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.scheduler); + + } + + } } Logger.i("java ping succeeds"); - double packetLoss = 1 - - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); +// double packetLoss = 1 - +// ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); //deleted by Clarence + if (packetLoss == Double.MIN_VALUE) { + packetLoss = 1 - + ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + } dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; @@ -505,6 +574,10 @@ private MeasurementResult executeHttpPingTask() throws MeasurementError { PingDesc pingTask = (PingDesc) this.measurementDesc; String errorMsg = ""; MeasurementResult result = null; + double packetLoss = Double.MIN_VALUE; //added by Clarence + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; try { long totalPingDelay = 0; @@ -525,12 +598,27 @@ private MeasurementResult executeHttpPingTask() throws MeasurementError { pingEndTime = System.currentTimeMillis(); httpClient.disconnect(); rrts.add((double) (pingEndTime - pingStartTime)); + packetLoss = 1 - + ((double) rrts.size() / (i+1)); + dataConsumed += pingTask.packetSizeByte * (i+1) * 2; + this.scheduler = this.getScheduler(); + if ( this.scheduler != null){ + IM_measurementResult = constructResult(rrts,packetLoss, + (i+1),PING_METHOD_HTTP); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.scheduler); + + } } Logger.i("HTTP get ping succeeds"); Logger.i("RTT is " + rrts.toString()); - double packetLoss = 1 - - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); +// double packetLoss = 1 +// - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + if (packetLoss == Double.MIN_VALUE) { + packetLoss = 1 - + ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + } dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; diff --git a/Mobilyzer/src/com/mobilyzer/measurements/SequentialTask.java b/Mobilyzer/src/com/mobilyzer/measurements/SequentialTask.java index 7dd1a38..bee358e 100644 --- a/Mobilyzer/src/com/mobilyzer/measurements/SequentialTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/SequentialTask.java @@ -169,6 +169,7 @@ public MeasurementResult[] call() throws MeasurementError { try { // futures=executor.invokeAll(this.tasks,timeout,TimeUnit.MILLISECONDS); for(MeasurementTask mt: tasks){ + mt.setScheduler(this.getScheduler()); if(stopFlag){ throw new MeasurementError("Cancelled"); } diff --git a/Mobilyzer/src/com/mobilyzer/measurements/TCPThroughputTask.java b/Mobilyzer/src/com/mobilyzer/measurements/TCPThroughputTask.java index 66a3550..e1faa26 100644 --- a/Mobilyzer/src/com/mobilyzer/measurements/TCPThroughputTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/TCPThroughputTask.java @@ -17,7 +17,9 @@ import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementScheduler; import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; @@ -26,6 +28,7 @@ import com.mobilyzer.util.PhoneUtils; import android.content.Context; +import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; @@ -88,6 +91,34 @@ public class TCPThroughputTask extends MeasurementTask { private long duration; private TaskProgress taskProgress; private volatile boolean stopFlag; + + private MeasurementScheduler scheduler = null; // added by Clarence + private TaskProgress Intermediate_TaskProgress = TaskProgress.COMPLETED; //added by Clarence + + //added by Clarence, add broadcast to send the intermediate results + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, MeasurementScheduler scheduler) { + this.scheduler = scheduler; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.scheduler.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } // class constructor public TCPThroughputTask(MeasurementDesc desc) { @@ -530,13 +561,17 @@ private void uplink() throws MeasurementError, IOException, InterruptedException long startTime = System.currentTimeMillis(); long endTime = startTime; + int data_limit_byte_up = (int)(((TCPThroughputDesc)measurementDesc).data_limit_mb_up *this.KBYTE*this.KBYTE); byte[] uplinkBuffer = new byte[((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes]; this.genRandomByteArray(uplinkBuffer); + + try { + long totalDuration = (long)(this.KSEC* ((TCPThroughputDesc)measurementDesc).duration_period_sec + @@ -550,6 +585,10 @@ private void uplink() throws MeasurementError, IOException, InterruptedException oStream.write(uplinkBuffer, 0, uplinkBuffer.length); oStream.flush(); endTime = System.currentTimeMillis(); + + + + this.totalSendSize += ((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes; if (this.DATA_LIMIT_ON && @@ -576,6 +615,9 @@ private void uplink() throws MeasurementError, IOException, InterruptedException byte [] resultMsg = new byte[this.BUFFER_SIZE]; int resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length); if (resultMsgLen > 0) { + + MeasurementResult IntermediateResult = null; //added by Clarence + String resultMsgStr = new String(resultMsg).substring(0, resultMsgLen); // Sample result string is "1111.11#2222.22#3333.33"; Logger.i("Uplink result from server is " + resultMsgStr); @@ -585,6 +627,23 @@ private void uplink() throws MeasurementError, IOException, InterruptedException sampleResult = Double.valueOf(tps_result_str[i]); this.samplingResults = this.insertWithOrder(this.samplingResults, sampleResult); + + //added by Clarence + this.scheduler = this.getScheduler(); + if (this.scheduler != null){ + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TCPThroughputTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + IntermediateResult.addResult("tcp_speed_results", this.samplingResults); + IntermediateResult.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); + IntermediateResult.addResult("duration", this.taskDuration); + IntermediateResult.addResult("server_version", this.serverVersion); + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.scheduler); + + } } } Logger.i("Total number of sampling result is " + this.samplingResults.size()); @@ -679,6 +738,9 @@ private void downlink() throws MeasurementError, IOException { * @param time period increment */ private void updateSize(int delta) { + + MeasurementResult IntermediateResult = null; //added by Clarence + double gtime = System.currentTimeMillis() - this.taskStartTime; //ignore slow start if (gtime<((TCPThroughputDesc)measurementDesc).slow_start_period_sec*this.KSEC) @@ -696,6 +758,24 @@ private void updateSize(int delta) { this.samplingResults = this.insertWithOrder(this.samplingResults, throughput); this.accumulativeSize = 0; this.startSampleTime = System.currentTimeMillis(); + + //added by Clarence + this.scheduler = this.getScheduler(); + if (this.scheduler != null){ + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TCPThroughputTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + IntermediateResult.addResult("tcp_speed_results", this.samplingResults); + IntermediateResult.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); + IntermediateResult.addResult("duration", time); + IntermediateResult.addResult("server_version", this.serverVersion); + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.scheduler); + + } + } } diff --git a/Mobilyzer/src/com/mobilyzer/measurements/TracerouteTask.java b/Mobilyzer/src/com/mobilyzer/measurements/TracerouteTask.java index 14fba2f..9803e3f 100755 --- a/Mobilyzer/src/com/mobilyzer/measurements/TracerouteTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/TracerouteTask.java @@ -14,9 +14,11 @@ package com.mobilyzer.measurements; +import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -42,8 +44,10 @@ import com.mobilyzer.Config; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementScheduler; import com.mobilyzer.MeasurementTask; import com.mobilyzer.PreemptibleMeasurementTask; +import com.mobilyzer.UpdateIntent; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; @@ -83,9 +87,37 @@ public class TracerouteTask extends MeasurementTask implements PreemptibleMeasur private long totalRunningTime; private int ttl; private int maxHopCount; + // Track data consumption for this task to avoid exceeding user's limit private long dataConsumed; + private MeasurementScheduler scheduler = null; // added by Clarence + private TaskProgress Intermediate_TaskProgress = TaskProgress.COMPLETED; //added by Clarence + + //added by Clarence, add broadcast to send the intermediate results + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, MeasurementScheduler scheduler) { + this.scheduler = scheduler; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.scheduler.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } /** * The description of the Traceroute measurement @@ -110,6 +142,8 @@ public static class TracerouteDesc extends MeasurementDesc { public String preCondition; private int parallelProbeNum; + + public TracerouteDesc(String key, Date startTime, Date endTime, double intervalSec, long count, long priority, int contextIntervalSec, Map params) @@ -323,6 +357,9 @@ public MeasurementResult[] call() throws MeasurementError { throw new MeasurementError("target " + target + " cannot be resolved"); } MeasurementResult result = null; + MeasurementResult IntermediateResult = null; //added by Clarence + + ExecutorService hopExecutorService = Executors.newFixedThreadPool(task.parallelProbeNum); @@ -343,11 +380,11 @@ public MeasurementResult[] call() throws MeasurementError { target); taskCompletionService.submit(new HopExecutor(task.pingsPerHop, command, hostIp, ttl)); ttl++; - } + } for (int tasksHandled = 0; tasksHandled < maxHopCount; tasksHandled++) { - + try { Future hopResult = taskCompletionService.take(); HopInfo hop = hopResult.get(); @@ -376,7 +413,9 @@ public MeasurementResult[] call() throws MeasurementError { new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), TracerouteTask.TYPE, System.currentTimeMillis() * 1000, taskProgress, this.measurementDesc); + result.addResult("num_hops", hop.ttl); + for (int i = 0; i < hopHosts.size(); i++) { HopInfo hopInfo = hopHosts.get(i); int hostIdx = 1; @@ -389,6 +428,7 @@ public MeasurementResult[] call() throws MeasurementError { } } Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray = new MeasurementResult[1]; mrArray[0] = result; return mrArray; @@ -397,6 +437,39 @@ public MeasurementResult[] call() throws MeasurementError { } } + //added by Clarence + this.scheduler = this.getScheduler(); + if (this.scheduler != null){ + + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TracerouteTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + + IntermediateResult.addResult("num_hops", hop.ttl); + + for (int i = 0; i < hopHosts.size(); i++) { + HopInfo IM_hopInfo = hopHosts.get(i); + int IM_hostIdx = 1; //added by Clarence + for (String IM_host : IM_hopInfo.hosts) { + IntermediateResult.addResult("hop_" + IM_hopInfo.ttl + "_addr_" + IM_hostIdx++, IM_host); + } + IntermediateResult.addResult("hop_" + IM_hopInfo.ttl + "_rtt_ms", String.format("%.3f", IM_hopInfo.rtt)); + + } + +// for (String IM_host :hop.hosts){ +// IntermediateResult.addResult("hop_" + hop.ttl + "_addr_" + IM_hostIdx++, IM_host); +// } +// IntermediateResult.addResult("hop_" + hop.ttl + "_rtt_ms", String.format("%.3f", hop.rtt)); + + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.scheduler); + } + + + } catch (InterruptedException e) { e.printStackTrace(); diff --git a/Mobilyzer/src/com/mobilyzer/measurements/UDPBurstTask.java b/Mobilyzer/src/com/mobilyzer/measurements/UDPBurstTask.java index ee5ffe4..2b85429 100644 --- a/Mobilyzer/src/com/mobilyzer/measurements/UDPBurstTask.java +++ b/Mobilyzer/src/com/mobilyzer/measurements/UDPBurstTask.java @@ -42,7 +42,7 @@ * * UDPBurstTask provides two types of measurements, Burst Up and Burst Down, described next. * - * 1. UDPBurst Up: the device sends sends a burst of UDPBurstCount UDP packets and waits for a + * 1. UDPBurst Up: the device sends a burst of UDPBurstCount UDP packets and waits for a * response from the server that includes the number of packets that the server received * * 2. UDPBurst Down: the device sends a request to a remote server on a UDP port and the server diff --git a/src/com/mobilyzer/APIRequestHandler.java b/src/com/mobilyzer/APIRequestHandler.java new file mode 100644 index 0000000..853826e --- /dev/null +++ b/src/com/mobilyzer/APIRequestHandler.java @@ -0,0 +1,180 @@ +/* Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; + +import com.mobilyzer.MeasurementScheduler.DataUsageProfile; +import com.mobilyzer.MeasurementScheduler.TaskStatus; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +/** + * @author Hongyi Yao (hyyao@umich.edu) + * Define message handler to process message request from API + */ +public class APIRequestHandler extends Handler { + MeasurementScheduler scheduler; + + /** + * Constructor for APIRequestHandler + * @param scheduler Parent context for this object + */ + public APIRequestHandler(MeasurementScheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public void handleMessage(Message msg) { + Bundle data = msg.getData(); + data.setClassLoader(scheduler.getApplicationContext().getClassLoader()); + String clientKey = data.getString(UpdateIntent.CLIENTKEY_PAYLOAD); + + MeasurementTask task = null; + String taskId = null; + int batteryThreshold = -1; + long interval = -1; + Intent intent = new Intent(); + DataUsageProfile profile = DataUsageProfile.NOTASSIGNED; + boolean isForced = (PhoneUtils.clientKeySet.size() == 1); + String account = null; + switch (msg.what) { + case Config.MSG_REGISTER_CLIENTKEY: + Logger.i("App " + clientKey + " registered"); + synchronized(PhoneUtils.clientKeySet) { + PhoneUtils.clientKeySet.add(clientKey); + } + break; + case Config.MSG_UNREGISTER_CLIENTKEY: + Logger.i("App " + clientKey + " unregistered"); + synchronized(PhoneUtils.clientKeySet) { + PhoneUtils.clientKeySet.remove(clientKey); + } + break; + case Config.MSG_SUBMIT_TASK: + task = (MeasurementTask) + data.getParcelable(UpdateIntent.MEASUREMENT_TASK_PAYLOAD); + if ( task != null ) { +// // Hongyi: for delay measurement +// task.getDescription().parameters.put("ts_scheduler_recv", +// String.valueOf(System.currentTimeMillis())); + + Logger.i("Request Handler: " + clientKey + " submit task " + task.getTaskId()); + scheduler.submitTask(task); + } + break; + case Config.MSG_CANCEL_TASK: + taskId = data.getString(UpdateIntent.TASKID_PAYLOAD); + if ( taskId != null && clientKey != null ) { + Logger.i("Request Handler: " + clientKey + " cancel task " + taskId); + scheduler.cancelTask(taskId, clientKey); + } + break; + case Config.MSG_SET_BATTERY_THRESHOLD: + batteryThreshold = data.getInt(UpdateIntent.BATTERY_THRESHOLD_PAYLOAD); + if ( batteryThreshold != -1 ) { + Logger.i("Request Handler: " + clientKey + " set battery threshold to " + + batteryThreshold); + scheduler.setBatteryThresh(isForced, batteryThreshold); + } + else { + Logger.e("Request Handler: didn't find battery threshold's value"); + } + break; + case Config.MSG_GET_BATTERY_THRESHOLD: + batteryThreshold = scheduler.getBatteryThresh(); + Logger.i("Request Handler: " + clientKey + " get battery threshold " + + batteryThreshold); + intent.setAction(UpdateIntent.BATTERY_THRESHOLD_ACTION + "." + clientKey); + intent.putExtra(UpdateIntent.BATTERY_THRESHOLD_PAYLOAD, batteryThreshold); + sendToClient(intent, clientKey, null); + break; + case Config.MSG_SET_CHECKIN_INTERVAL: + interval = data.getLong(UpdateIntent.CHECKIN_INTERVAL_PAYLOAD); + if ( interval != -1 ) { + Logger.i("Request Handler: " + clientKey + " set checkin interval to " + + interval); + scheduler.setCheckinInterval(isForced, interval); + } + else { + Logger.e("Request Handler: didn't find checkin interval's value"); + } + break; + case Config.MSG_GET_CHECKIN_INTERVAL: + interval = scheduler.getCheckinInterval(); + Logger.i("Request Handler: " + clientKey + " get checkin interval " + + interval); + intent.setAction(UpdateIntent.CHECKIN_INTERVAL_ACTION + "." + clientKey); + intent.putExtra(UpdateIntent.CHECKIN_INTERVAL_PAYLOAD, interval); + sendToClient(intent, clientKey, null); + break; + case Config.MSG_GET_TASK_STATUS: + taskId = data.getString(UpdateIntent.TASKID_PAYLOAD); + TaskStatus taskStatus = scheduler.getTaskStatus(taskId); + Logger.i("Request Handler: " + clientKey + " get task status for taskId " + + taskId + " " + taskStatus); + intent.setAction(UpdateIntent.TASK_STATUS_ACTION + "." + clientKey); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, taskId); + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, taskStatus); + sendToClient(intent, clientKey, taskId); + break; + case Config.MSG_SET_DATA_USAGE: + profile = (DataUsageProfile) + data.getSerializable(UpdateIntent.DATA_USAGE_PAYLOAD); + if ( profile != null ) { + Logger.i("Request Handler: " + clientKey + " set data usage to " + + profile ); + scheduler.setDataUsageLimit(isForced, profile); + } + else { + Logger.e("Scheduler: didn't found data usage profile's value"); + } + break; + case Config.MSG_GET_DATA_USAGE: + profile = scheduler.getDataUsageProfile(); + Logger.i("Request Handler: " + clientKey + " get data usage " + profile); + intent.setAction(UpdateIntent.DATA_USAGE_ACTION + "." + clientKey); + intent.putExtra(UpdateIntent.DATA_USAGE_PAYLOAD, profile); + sendToClient(intent, clientKey, taskId); + case Config.MSG_SET_AUTH_ACCOUNT: + account = data.getString(UpdateIntent.AUTH_ACCOUNT_PAYLOAD); + if (account != null) { + Logger.i("Request Handler: " + clientKey + " set authenticate account" + + " to " + account); + scheduler.setAuthenticateAccount(account); + } + break; + case Config.MSG_GET_AUTH_ACCOUNT: + account = scheduler.getAuthenticateAccount(); + Logger.i("Request Handler: " + clientKey + " get authenticate account " + + account); + intent.setAction(UpdateIntent.AUTH_ACCOUNT_ACTION + "." + clientKey); + intent.putExtra(UpdateIntent.AUTH_ACCOUNT_PAYLOAD, account); + sendToClient(intent, clientKey, taskId); + default: + break; + } + } + + private void sendToClient(Intent intent, String clientKey, String taskId) { + if ( taskId != null ) { + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, taskId); + } + scheduler.sendBroadcast(intent); + } +} \ No newline at end of file diff --git a/src/com/mobilyzer/AccountSelector.java b/src/com/mobilyzer/AccountSelector.java new file mode 100755 index 0000000..c122179 --- /dev/null +++ b/src/com/mobilyzer/AccountSelector.java @@ -0,0 +1,305 @@ +/* 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; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; + +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.ClientPNames; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.DefaultHttpClient; + +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + + +/** + * Helper class for google account checkins + */ +public class AccountSelector { + public static final String ACCOUNT_TYPE = "com.google"; + private static final String ACCOUNT_NAME = "@google.com"; + // The authentication period in milliseconds + private static final long AUTHENTICATE_PERIOD_MSEC = 24 * 3600 * 1000; + private Context context; + private String authToken = null; + private ExecutorService checkinExecutor = null; + private Future checkinFuture = null; + private long lastAuthTime = 0; + private boolean authImmediately = false; + private PhoneUtils phoneUtils; + + private boolean isAnonymous = true; + public boolean isAnonymous() { return isAnonymous; } + + public AccountSelector(Context context) { + this.context = context; + this.checkinExecutor = Executors.newFixedThreadPool(1); + this.phoneUtils = PhoneUtils.getPhoneUtils(); + } + + /** Returns the Future to monitor the checkin progress */ + public synchronized Future getCheckinFuture() { + return this.checkinFuture; + } + + /** After checkin finishes, the client of AccountSelector SHOULD reset checkinFuture */ + public synchronized void resetCheckinFuture() { + this.checkinFuture = null; + } + + /** Shuts down the executor thread */ + public void shutDown() { + // shutdown() removes all previously submitted task and no new tasks are accepted + this.checkinExecutor.shutdown(); + // shutdownNow stops all currently executing tasks + this.checkinExecutor.shutdownNow(); + } + + /** Allows clients of AccountSelector to request an authentication upon the next call + * to authenticate() */ + public synchronized void setAuthImmediately(boolean val) { + this.authImmediately = val; + } + + private synchronized boolean shouldAuthImmediately() { + return this.authImmediately; + } + + private synchronized void setLastAuthTime(long lastTime) { + this.lastAuthTime = lastTime; + } + + private synchronized long getLastAuthTime() { + return this.lastAuthTime; + } + + /** + * Return the list of account names for users to select + */ + public static String[] getAccountList(Context context) { + AccountManager accountManager = AccountManager.get(context.getApplicationContext()); + Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); + int numAccounts = accounts == null ? 1 : accounts.length + 1; + String[] accountNames = new String[numAccounts]; + for (int i = 0 ; i < accounts.length ; i++) { + accountNames[i] = accounts[i].name; + } + accountNames[numAccounts - 1] = Config.DEFAULT_USER; + return accountNames; + } + + /** Starts an authentication request */ + public void authenticate() + throws OperationCanceledException, AuthenticatorException, IOException { + Logger.i("AccountSelector.authenticate() running"); + /* We only need to authenticate every AUTHENTICATE_PERIOD_MILLI milliseconds, during + * which we can reuse the cookie. If authentication fails due to expired + * authToken, the client of AccountSelector can call authImmedately() to request + * authenticate() upon the next checkin + */ + long authTimeLast = this.getLastAuthTime(); + long timeSinceLastAuth = System.currentTimeMillis() - authTimeLast; + if (!this.shouldAuthImmediately() && authTimeLast != 0 && + (timeSinceLastAuth < AUTHENTICATE_PERIOD_MSEC)) { + return; + } + + Logger.i("Authenticating. Last authentication is " + + timeSinceLastAuth / 1000 / 60 + " minutes ago. "); + + AccountManager accountManager = AccountManager.get(context.getApplicationContext()); + if (this.authToken != null) { + // There will be no effect on the token if it is still valid + Logger.i("Invalidating token"); + accountManager.invalidateAuthToken(ACCOUNT_TYPE, this.authToken); + } + + Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); + Logger.i("Got " + accounts.length + " accounts"); + + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + String selectedAccount = prefs.getString(Config.PREF_KEY_SELECTED_ACCOUNT, null); + Logger.i("Selected account = " + selectedAccount); + + final String defaultUserName = Config.DEFAULT_USER; + isAnonymous = true; + if (selectedAccount != null && selectedAccount.equals(defaultUserName)) { + return; + } + + if (accounts != null && accounts.length > 0) { + // Default account should be the Anonymous account + Account accountToUse = new Account(Config.DEFAULT_USER, ACCOUNT_TYPE); + if (!accounts[accounts.length-1].name.equals(defaultUserName)) { + for (Account account : accounts) { + if (account.name.equals(defaultUserName)) { + accountToUse = account; + break; + } + } + } + if (selectedAccount != null) { + for (Account account : accounts) { + if (account.name.equals(selectedAccount)) { + accountToUse = account; + break; + } + } + } + + isAnonymous = accountToUse.name.equals(defaultUserName); + + if (isAnonymous) { + Logger.d("Skipping authentication as account is " + defaultUserName); + return; + } + + Logger.i("Trying to get auth token for " + accountToUse); + AccountManagerFuture future = accountManager.getAuthToken( + accountToUse, "ah", false, new AccountManagerCallback() { + @Override + public void run(AccountManagerFuture result) { + Logger.i("AccountManagerCallback invoked"); + try { + getAuthToken(result); + } catch (RuntimeException e) { + Logger.e("Failed to get authToken", e); + /* TODO(Wenjie): May ask the user whether to quit the app nicely here if a number + * of trials have been made and failed. Since Speedometer is basically useless + * without checkin + */ + } + }}, + null); + Logger.i("AccountManager.getAuthToken returned " + future); + } else { + throw new RuntimeException("No google account found"); + } + } + + private void getAuthToken(AccountManagerFuture result) { + Logger.i("getAuthToken() called, result " + result); + String errMsg = "Failed to get login cookie. "; + Bundle bundle; + try { + bundle = result.getResult(); + Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT); + if (intent != null) { + // User input required. (A UI will pop up for user's consent to allow + // this app access account information.) + Logger.i("Starting account manager activity"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(intent); + } else { + Logger.i("Executing getCookie task"); + synchronized (this) { + this.authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + this.checkinFuture = checkinExecutor.submit(new GetCookieTask()); + } + } + } catch (OperationCanceledException e) { + Logger.e(errMsg, e); + throw new RuntimeException("Can't get login cookie", e); + } catch (AuthenticatorException e) { + Logger.e(errMsg, e); + throw new RuntimeException("Can't get login cookie", e); + } catch (IOException e) { + Logger.e(errMsg, e); + throw new RuntimeException("Can't get login cookie", e); + } + } + + private class GetCookieTask implements Callable { + @Override + public Cookie call() { + Logger.i("GetCookieTask running: " + authToken); + DefaultHttpClient httpClient = new DefaultHttpClient(); + boolean success = false; + try { + String loginUrlPrefix = phoneUtils.getServerUrl() + + "/_ah/login?continue=" + phoneUtils.getServerUrl() + + "&action=Login&auth="; + // Don't follow redirects + httpClient.getParams().setBooleanParameter( + ClientPNames.HANDLE_REDIRECTS, false); + HttpGet httpGet = new HttpGet(loginUrlPrefix + authToken); + HttpResponse response; + Logger.i("Accessing: " + loginUrlPrefix + authToken); + response = httpClient.execute(httpGet); + if (response.getStatusLine().getStatusCode() != 302) { + // Response should be a redirect to the "continue" URL. + Logger.e("Failed to get login cookie: " + + loginUrlPrefix + " returned unexpected error code " + + response.getStatusLine().getStatusCode()); + throw new RuntimeException("Failed to get login cookie: " + + loginUrlPrefix + " returned unexpected error code " + + response.getStatusLine().getStatusCode()); + } + + Logger.i("Got " + + httpClient.getCookieStore().getCookies().size() + " cookies back"); + + for (Cookie cookie : httpClient.getCookieStore().getCookies()) { + Logger.i("Checking cookie " + cookie); + if (cookie.getName().equals("SACSID") + || cookie.getName().equals("ACSID")) { + Logger.i("Got cookie " + cookie); + setLastAuthTime(System.currentTimeMillis()); + success = true; + return cookie; + } + } + Logger.e("No (S)ASCID cookies returned"); + throw new RuntimeException("Failed to get login cookie: " + + loginUrlPrefix + " did not return any (S)ACSID cookie"); + } catch (ClientProtocolException e) { + Logger.e("Failed to get login cookie", e); + throw new RuntimeException("Failed to get login cookie", e); + } catch (IOException e) { + Logger.e("Failed to get login cookie", e); + throw new RuntimeException("Failed to get login cookie", e); + } finally { + httpClient.getParams().setBooleanParameter( + ClientPNames.HANDLE_REDIRECTS, true); + if (!success) { + resetCheckinFuture(); + } + } + } + } + + +} diff --git a/src/com/mobilyzer/Checkin.java b/src/com/mobilyzer/Checkin.java new file mode 100755 index 0000000..4397ecc --- /dev/null +++ b/src/com/mobilyzer/Checkin.java @@ -0,0 +1,546 @@ +/* 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; + + +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.os.AsyncTask; + +import org.apache.http.HttpVersion; +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.cookie.Cookie; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.HTTP; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.mobilyzer.gcm.GCMManager; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Handles checkins with the server. + */ +public class Checkin { + private static final int POST_TIMEOUT_MILLISEC = 20 * 1000; + private Context context; + private Date lastCheckin; + private volatile Cookie authCookie = null; + private AccountSelector accountSelector = null; + PhoneUtils phoneUtils; + String gcm_registraion_id; + + public Checkin(Context context) { + phoneUtils = PhoneUtils.getPhoneUtils(); + this.context = context; + this.gcm_registraion_id=""; + } + + /** Shuts down the checkin thread */ + public void shutDown() { + if (this.accountSelector != null) { + this.accountSelector.shutDown(); + } + } + + /** Return a fake authentication cookie for a test server instance */ + private Cookie getFakeAuthCookie() { + BasicClientCookie cookie = new BasicClientCookie( + "dev_appserver_login", + "test@nobody.com:False:185804764220139124118"); + cookie.setDomain(".google.com"); + cookie.setVersion(1); + cookie.setPath("/"); + cookie.setSecure(false); + return cookie; + } + + public Date lastCheckinTime() { + return this.lastCheckin; + } + + public List checkin(ResourceCapManager resourceCapManager, GCMManager gcm) throws IOException { + Logger.i("Checkin.checkin() called"); + boolean checkinSuccess = false; + gcm_registraion_id=gcm.getRegistrationId(); + try { + JSONObject status = new JSONObject(); + DeviceInfo info = phoneUtils.getDeviceInfo(); + // TODO(Wenjie): There is duplicated info here, such as device ID. + status.put("id", info.deviceId); + status.put("manufacturer", info.manufacturer); + status.put("model", info.model); + status.put("os", info.os); + /** + * TODO: checkin task don't belongs to any app. So we just fill + * request_app field with server task key + */ + + DeviceProperty deviceProperty=phoneUtils.getDeviceProperty(Config.CHECKIN_KEY); + deviceProperty.setRegistrationId(gcm.getRegistrationId()); + Logger.d("Checkin-> GCMManager: "+gcm.getRegistrationId()); + + status.put("properties", MeasurementJsonConvertor.encodeToJson(deviceProperty)); + + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + resourceCapManager.updateDataUsage(ResourceCapManager.PHONEUTILCOST); + } + + Logger.d(status.toString()); + + Logger.d("Checkin: "+status.toString()); + + + String result = serviceRequest("checkin", status.toString()); + Logger.d("Checkin result: " + result); + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + resourceCapManager.updateDataUsage(result.length()); + } + + // Parse the result + Vector schedule = new Vector(); + JSONArray jsonArray = new JSONArray(result); + + + for (int i = 0; i < jsonArray.length(); i++) { + Logger.d("Parsing index " + i); + JSONObject json = jsonArray.optJSONObject(i); + Logger.d("Value is " + json); + // checkin task must support + if (json != null && + MeasurementTask.getMeasurementTypes().contains(json.get("type"))) { + try { + MeasurementTask task = + MeasurementJsonConvertor.makeMeasurementTaskFromJson(json); + Logger.i(MeasurementJsonConvertor.toJsonString(task.measurementDesc)); + + schedule.add(task); + } catch (IllegalArgumentException e) { + Logger.w("Could not create task from JSON: " + e); + // Just skip it, and try the next one + } + } + } + + this.lastCheckin = new Date(); + Logger.i("Checkin complete, got " + schedule.size() + + " new tasks"); + checkinSuccess = true; + return schedule; + } catch (JSONException e) { + Logger.e("Got exception during checkin", e); + throw new IOException("There is exception during checkin()"); + } catch (IOException e) { + Logger.e("Got exception during checkin", e); + throw e; + } finally { + if (!checkinSuccess) { + // Failure probably due to authToken expiration. Will authenticate upon next checkin. + this.accountSelector.setAuthImmediately(true); + this.authCookie = null; + } + } + } + + + /** + * Read in the results of tasks completed to date from a file, then clear the file. + * + * @return The results as a JSONArray, ready for sending to the server. + */ + private synchronized JSONArray readResultsFromFile() { + + JSONArray results = new JSONArray(); + try { + Logger.d("Loading results from disk: "+context.getFilesDir()); + + FileInputStream inputstream = context.openFileInput("results"); + InputStreamReader streamreader = new InputStreamReader(inputstream); + BufferedReader bufferedreader = new BufferedReader(streamreader); + + String line; + int count = 0; + while ((line = bufferedreader.readLine()) != null) { + JSONObject jsonTask; + try { + jsonTask = new JSONObject(line); + count++; + results.put(jsonTask); + } catch (JSONException e) { + Logger.e("", e); + } + } + Logger.i("Got " + count + " results from file"); + + bufferedreader.close(); + streamreader.close(); + inputstream.close(); + + // delete file once done, to avoid uploading results twice + context.deleteFile("results"); + + + } catch (FileNotFoundException e) { + Logger.e("", e); + } catch (IOException e) { + Logger.e("", e); + } + return results; + } + + public void uploadMeasurementResult(Vector finishedTasks, ResourceCapManager resourceCapManager) + throws IOException { + JSONArray resultArray = readResultsFromFile(); + for (MeasurementResult result : finishedTasks) { + try { + resultArray.put(MeasurementJsonConvertor.encodeToJson(result)); + } catch (JSONException e1) { + Logger.e("Error when adding " + result); + } + } + + JSONArray chunckedArray= new JSONArray(); + int i=0; + for (;i { + + @Override + protected String doInBackground(String... results) { + if(results.length!=1){ + return ""; + } + String r=results[0]; + String response=""; + try { + response=serviceRequest("postmeasurement", r); + } catch (IOException e) { + Logger.e("Failed to upload local event: "+e.getMessage()); + } + return response; + } + } + + + public void uploadSingleMeasurementResult(MeasurementResult result, ResourceCapManager resourceCapManager) + throws IOException, InterruptedException, ExecutionException { + + result.getDeviceProperty().registrationId=gcm_registraion_id; + try { + JSONArray resultArray= new JSONArray(); + resultArray.put(MeasurementJsonConvertor.encodeToJson(result)); + Logger.d("Single Measurement result converted to json: "+resultArray.toString()); + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + resourceCapManager.updateDataUsage(resultArray.toString().length()); + } + + String response=new NotUIBlockingResultUploader().execute(resultArray.toString()).get(); +// String response = serviceRequest("postmeasurement", resultJson.toString()); + try { + JSONObject responseJson = new JSONObject(response); + if (!responseJson.getBoolean("success")) { + throw new IOException("Failure posting single measurement result"); + } + } catch (JSONException e) { + throw new IOException(e.getMessage()); + } + } catch (JSONException e1) { + Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete"); + } + Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete"); + + } + + + /** + * Used to generate SSL sockets. + */ + class MySSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public MySSLSocketFactory(KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, + KeyStoreException, UnrecoverableKeyException { + super(truststore); + + X509TrustManager tm = new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + // Do nothing + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + // Do nothing + } + }; + + sslContext.init(null, new TrustManager[] { tm }, null); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, + boolean autoClose) throws IOException, UnknownHostException { + return sslContext.getSocketFactory().createSocket(socket, host, port, + autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } + } + + /** + * Return an appropriately-configured HTTP client. + */ + private HttpClient getNewHttpClient() { + DefaultHttpClient client; + try { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + + SSLSocketFactory sf = new MySSLSocketFactory(trustStore); + sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + + HttpParams params = new BasicHttpParams(); + HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); + HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); + + HttpConnectionParams.setConnectionTimeout(params, POST_TIMEOUT_MILLISEC); + HttpConnectionParams.setSoTimeout(params, POST_TIMEOUT_MILLISEC); + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", PlainSocketFactory + .getSocketFactory(), 80)); + registry.register(new Scheme("https", sf, 443)); + + ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, + registry); + client = new DefaultHttpClient(ccm, params); + } catch (Exception e) { + Logger.w("Unable to create SSL HTTP client", e); + client = new DefaultHttpClient(); + } + + // TODO(mdw): For some reason this is not sending the cookie to the + // test server, probably because the cookie itself is not properly + // initialized. Below I manually set the Cookie header instead. + CookieStore store = new BasicCookieStore(); + store.addCookie(authCookie); + client.setCookieStore(store); + return client; + } + + public String serviceRequest(String url, String jsonString) + throws IOException { + + if (this.accountSelector == null) { + accountSelector = new AccountSelector(context); + } + if (!accountSelector.isAnonymous()) { + synchronized (this) { + if (authCookie == null) { + if (!checkGetCookie()) { + throw new IOException("No authCookie yet"); + } + } + } + } + + HttpClient client = getNewHttpClient(); + String fullurl = (accountSelector.isAnonymous() ? + phoneUtils.getAnonymousServerUrl() : + phoneUtils.getServerUrl()) + "/" + url; + Logger.i("Checking in to " + fullurl); + HttpPost postMethod = new HttpPost(fullurl); + + StringEntity se; + try { + se = new StringEntity(jsonString); + } catch (UnsupportedEncodingException e) { + throw new IOException(e.getMessage()); + } + postMethod.setEntity(se); + postMethod.setHeader("Accept", "application/json"); + postMethod.setHeader("Content-type", "application/json"); + if (!accountSelector.isAnonymous()) { + // TODO(mdw): This should not be needed + postMethod.setHeader("Cookie", authCookie.getName() + "=" + authCookie.getValue()); + } + + ResponseHandler responseHandler = new BasicResponseHandler(); + Logger.i("Sending request: " + fullurl); + String result = client.execute(postMethod, responseHandler); + return result; + } + + /** + * Initiates the process to get the authentication cookie for the user account. + * Returns immediately. + */ + public synchronized void getCookie() { + if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) { + Logger.i("Setting fakeAuthCookie"); + authCookie = getFakeAuthCookie(); + return; + } + if (this.accountSelector == null) { + accountSelector = new AccountSelector(context); + } + + try { + // Authenticates if there are no ongoing ones + if (accountSelector.getCheckinFuture() == null) { + accountSelector.authenticate(); + } + } catch (OperationCanceledException e) { + Logger.e("Unable to get auth cookie", e); + } catch (AuthenticatorException e) { + Logger.e("Unable to get auth cookie", e); + } catch (IOException e) { + Logger.e("Unable to get auth cookie", e); + } + } + + /** + * Resets the checkin variables in AccountSelector + * */ + public void initializeAccountSelector() { + accountSelector.resetCheckinFuture(); + accountSelector.setAuthImmediately(false); + } + + private synchronized boolean checkGetCookie() { + if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) { + authCookie = getFakeAuthCookie(); + return true; + } + Future getCookieFuture = accountSelector.getCheckinFuture(); + if (getCookieFuture == null) { + Logger.i("checkGetCookie called too early"); + return false; + } + if (getCookieFuture.isDone()) { + try { + authCookie = getCookieFuture.get(); + Logger.i("Got authCookie: " + authCookie); + return true; + } catch (InterruptedException e) { + Logger.e("Unable to get auth cookie", e); + return false; + } catch (ExecutionException e) { + Logger.e("Unable to get auth cookie", e); + return false; + } + } else { + Logger.i("getCookieFuture is not yet finished"); + return false; + } + } + + +} diff --git a/src/com/mobilyzer/Config.java b/src/com/mobilyzer/Config.java new file mode 100644 index 0000000..cb214d7 --- /dev/null +++ b/src/com/mobilyzer/Config.java @@ -0,0 +1,109 @@ +package com.mobilyzer; + + +/** + * The system defaults. + */ + +public interface Config { + // Important: keep same with the version_code and version_name in strings.xml + public static final String version = "3"; + /** + * Strings migrated from string.xml + */ + public static final String SERVER_URL = "https://openmobiledata.appspot.com"; + public static final String ANONYMOUS_SERVER_URL = "https://openmobiledata.appspot.com/anonymous"; + public static final String TEST_SERVER_URL = ""; + public static final String DEFAULT_USER = "Anonymous"; + + public static final int MAX_TASK_QUEUE_SIZE = 100; + + public static final String USER_AGENT = "Mobilyzer-" + version + " (Linux; Android)"; + public static final String PING_EXECUTABLE = "ping"; + public static final String PING6_EXECUTABLE = "ping6"; + + public static final String SERVER_TASK_CLIENT_KEY = "LibraryServerTask"; + public static final String CHECKIN_KEY = "MobilyzerCheckin"; + + public static final String TASK_STARTED = "TASK_STARTED"; + public static final String TASK_FINISHED = "TASK_FINISHED"; + public static final String TASK_PAUSED = "TASK_PAUSED"; + public static final String TASK_RESUMED = "TASK_RESUMED"; + public static final String TASK_CANCELED = "TASK_CENCELED"; + public static final String TASK_STOPPED = "TASK_STOPPED"; + public static final String TASK_RESCHEDULED = "TASK_RESCHEDULED"; + + + /** Types for message between API and scheduler**/ + public static final int MSG_SUBMIT_TASK = 1; + public static final int MSG_RESULT = 2; + public static final int MSG_CANCEL_TASK = 3; + public static final int MSG_SET_BATTERY_THRESHOLD = 4; + public static final int MSG_GET_BATTERY_THRESHOLD = 5; + public static final int MSG_SET_CHECKIN_INTERVAL = 6; + public static final int MSG_GET_CHECKIN_INTERVAL = 7; + public static final int MSG_GET_TASK_STATUS = 8; + public static final int MSG_SET_DATA_USAGE = 9; + public static final int MSG_GET_DATA_USAGE = 10; + public static final int MSG_REGISTER_CLIENTKEY = 11; + public static final int MSG_UNREGISTER_CLIENTKEY = 12; + public static final int MSG_SET_AUTH_ACCOUNT = 13; + public static final int MSG_GET_AUTH_ACCOUNT = 14; + + /** The default battery level if we cannot read it from the system */ + public static final int DEFAULT_BATTERY_LEVEL = 0; + /** The default maximum battery level if we cannot read it from the system */ + public static final int DEFAULT_BATTERY_SCALE = 100; + + /** Tasks expire in a bit more than two days. Expired tasks will be removed from the scheduler */ + public static final long TASK_EXPIRATION_MSEC = 2 * 24 * 3600 * 1000 + 1800 * 1000; + /** Default interval in seconds between system measurements of a given measurement type */ + public static final double DEFAULT_SYSTEM_MEASUREMENT_INTERVAL_SEC = 15 * 60; + /** Default interval in seconds between context collection */ + public static final int DEFAULT_CONTEXT_INTERVAL_SEC = 5; + public static final int MAX_CONTEXT_INFO_COLLECTIONS_PER_TASK = 120; + + + + public static final int DEFAULT_DNS_COUNT_PER_MEASUREMENT = 1; + public static final int PING_COUNT_PER_MEASUREMENT = 10; + public static final float PING_FILTER_THRES = (float) 1.4; + public static final double DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC = 0.5; + + + public static final int TRACEROUTE_TASK_DURATION = 4 * 30 * 500; + public static final int DEFAULT_DNS_TASK_DURATION = 0; + public static final int DEFAULT_HTTP_TASK_DURATION = 0; + public static final int DEFAULT_PING_TASK_DURATION = PING_COUNT_PER_MEASUREMENT * 500; + public static final int DEFAULT_UDPBURST_DURATION = 30 * 1000; + public static final int DEFAULT_PARALLEL_TASK_DURATION = 60 * 1000; + public static final int DEFAULT_TASK_DURATION_TIMEOUT = 60 * 1000; + public static final int DEFAULT_RRC_TASK_DURATION = 30 * 60 * 1000; + public static final int MAX_TASK_DURATION = 15 * 60 * 1000;//TODO + + + // Keys in SharedPrefernce + public static final String PREF_KEY_SELECTED_ACCOUNT = "PREF_KEY_SELECTED_ACCOUNT"; + public static final String PREF_KEY_BATTERY_THRESHOLD = "PREF_KEY_BATTERY_THRESHOLD"; + public static final String PREF_KEY_CHECKIN_INTERVAL = "PREF_KEY_CHECKIN_INTERVAL"; + public static final String PREF_KEY_DATA_USAGE_PROFILE = "PREF_KEY_DATA_USAGE_PROFILE"; + + + public static final int MIN_BATTERY_THRESHOLD = 20; + public static final int MAX_BATTERY_THRESHOLD = 100; + public static final int DEFAULT_BATTERY_THRESH_PRECENT = 60; + + // The default checkin interval in seconds + public static final long DEFAULT_CHECKIN_INTERVAL_SEC = 60 * 60L; + public static final long MIN_CHECKIN_INTERVAL_SEC = 3600L; + public static final long MAX_CHECKIN_INTERVAL_SEC = 24 * 3600L; + public static final long MIN_CHECKIN_RETRY_INTERVAL_SEC = 20L; + public static final long MAX_CHECKIN_RETRY_INTERVAL_SEC = 60L; + public static final int MAX_CHECKIN_RETRY_COUNT = 3; + public static final long PAUSE_BETWEEN_CHECKIN_CHANGE_MSEC = 1 * 60 * 1000L; + + public static final int DEFAULT_DATA_MONITOR_PERIOD_DAY= 1; + + // Reschedule delay for RRC task + public static final long RESCHEDULE_DELAY = 20*60*1000; +} diff --git a/src/com/mobilyzer/ContextCollector.java b/src/com/mobilyzer/ContextCollector.java new file mode 100644 index 0000000..b11b703 --- /dev/null +++ b/src/com/mobilyzer/ContextCollector.java @@ -0,0 +1,212 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; +import android.annotation.SuppressLint; +import android.net.TrafficStats; +import com.mobilyzer.util.PhoneUtils; + +/** + * + * @author Jack Jia, Ashkan Nikravesh (ashnik@umich.edu) Collects context information periodically + * (using a Timer). User can specify the interval. + */ +public class ContextCollector { + + private volatile ArrayList> contextResultArray; + private PhoneUtils phoneUtils; + private int interval; + private Timer timer; + private volatile boolean isRunning; + private int count; + public volatile String ipConnectivity = ""; + public volatile String dnsConnectivity = ""; + + private long prevSend; + private long prevRecv; + private long prevPktSend; + private long prevPktRecv; + + + public ContextCollector() { + phoneUtils = PhoneUtils.getPhoneUtils(); + this.isRunning = false; + this.timer = new Timer(); + contextResultArray = new ArrayList>(); + count = 0; + + prevSend = -1; + prevRecv = -1; + prevPktSend = -1; + prevPktRecv = -1; + } + + /** + * this function sets the interval of context collection (in seconds) + * + * @param intervalSecond time between each context info snapshot + */ + public void setInterval(int intervalSecond) { + this.interval = intervalSecond; + if (intervalSecond <= 0) { + this.interval = Config.DEFAULT_CONTEXT_INTERVAL_SEC; + } + } + + /** + * called by the timer and return the current context info of device + * + * @return a hash map that contains all the context data + */ + @SuppressLint("NewApi") + private HashMap getCurrentContextInfo() { + HashMap currentContext = new HashMap();; + + + long intervalPktSend = 0; + long intervalPktRecv = 0; + long intervalSend = 0; + long intervalRecv = 0; + + long sendBytes = TrafficStats.getTotalTxBytes(); + long recvBytes = TrafficStats.getTotalRxBytes(); + long sendPkt = TrafficStats.getTotalTxPackets(); + long recvPkt = TrafficStats.getTotalRxPackets(); + + if (prevSend != -1 && prevRecv != -1) { + intervalSend = sendBytes - prevSend; + intervalRecv = recvBytes - prevRecv; + } + + if (prevPktSend != -1 && prevPktRecv != -1) { + intervalPktSend = sendPkt - prevPktSend; + intervalPktRecv = recvPkt - prevPktRecv; + } + // we only return the context info if (1) it's the first time it gets called (2) we have + // change in the amount of packet/byte sent/received. + if (prevSend == -1 || prevRecv == -1 || prevPktSend == -1 || prevPktRecv == -1 + || intervalSend != 0 || intervalRecv != 0 || intervalPktSend != 0 || intervalPktRecv != 0) { + currentContext.put("timestamp", (System.currentTimeMillis() * 1000) + ""); +// currentContext.put("rssi", phoneUtils.getCurrentRssi() + ""); + currentContext.put("inc_total_bytes_send", intervalSend + ""); + currentContext.put("inc_total_bytes_recv", intervalRecv + ""); + currentContext.put("inc_total_pkt_send", intervalPktSend + ""); + currentContext.put("inc_total_pkt_recv", intervalPktRecv + ""); + currentContext.put("battery_level", phoneUtils.getCurrentBatteryLevel() + ""); + } + + prevSend = sendBytes; + prevRecv = recvBytes; + prevPktSend = sendPkt; + prevPktRecv = recvPkt; + + return currentContext; + } + + /** + * Starts the context collection timer task. It should be called when a measurement task gets + * started. + * + * @return false if the collector is already running. + */ + public boolean startCollector() { + if (isRunning) { + return false; + } + isRunning = true; + timer.scheduleAtFixedRate(timerTask, 0, interval * 1000); + return true; + + + } + + /** + * Stops the context collection task. It attaches the current context data to the results + * + * @return array of all context info collected at specific time intervals + */ + public ArrayList> stopCollector() { + if (!isRunning) { + return null; + } + timerTask.cancel(); + timer.cancel(); + isRunning = false; + HashMap currentContext = getCurrentContextInfo(); + if (currentContext.size() != 0) { + contextResultArray.add(currentContext); + } + +// if(ipConnectivity.equals("")){ +// ipConnectivity = phoneUtils.getIpConnectivity(); +// } +// if(dnsConnectivity.equals("")){ +// dnsConnectivity = phoneUtils.getDnResolvability(); +// } + if(ipConnectivity.equals("")){ + ipConnectivity = "NOT SUPPORTED"; + } + if(dnsConnectivity.equals("")){ + dnsConnectivity = "NOT SUPPORTED"; + } + + return contextResultArray; + } + + /** + * Return the current Ip connectivity + * + * @return A string that represents the current ip connectivity. + */ + public String getCurrentIPConnectivity() { + return ipConnectivity; + } + + /** + * Return the current DNS resolvability + * + * @return A string that represents the current DNS resolvability. + */ + public String getCurrentDNSConnectivity() { + return dnsConnectivity; + } + + private TimerTask timerTask = new TimerTask() { + @Override + public void run() { + if (ContextCollector.this.count < Config.MAX_CONTEXT_INFO_COLLECTIONS_PER_TASK) { + HashMap currentContext = getCurrentContextInfo(); + if (currentContext.size() != 0) { + contextResultArray.add(currentContext); + ContextCollector.this.count++; + + if(ipConnectivity.equals("")){ + ipConnectivity = phoneUtils.getIpConnectivity(); + } + if(dnsConnectivity.equals("")){ + dnsConnectivity = phoneUtils.getDnResolvability(); + } + + + } + } + } + }; + + +} diff --git a/src/com/mobilyzer/DeviceInfo.java b/src/com/mobilyzer/DeviceInfo.java new file mode 100755 index 0000000..0e08553 --- /dev/null +++ b/src/com/mobilyzer/DeviceInfo.java @@ -0,0 +1,28 @@ +/* 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; + + +/** + * POJO class containing static device information. @see{@link DeviceProperty} + */ +public class DeviceInfo { + public String deviceId; + public String user; + public String manufacturer; + public String model; + public String os; +} diff --git a/src/com/mobilyzer/DeviceProperty.java b/src/com/mobilyzer/DeviceProperty.java new file mode 100755 index 0000000..ff7885b --- /dev/null +++ b/src/com/mobilyzer/DeviceProperty.java @@ -0,0 +1,220 @@ +/* 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; + + +import java.util.ArrayList; +import java.util.HashSet; + +import android.os.Parcel; +import android.os.Parcelable; + + + +/** + * POJO class containing dynamic information about the device + * @see DeviceInfo + */ +public class DeviceProperty implements Parcelable { + + public String deviceId; + public String appVersion; + public long timestamp; + public String osVersion; + public String ipConnectivity; + public String dnResolvability; + public GeoLocation location; + public String locationType; + public String networkType; + public String carrier; + // ISO country code equivalent of the current registered operator's MCC + public String countryCode; + public int batteryLevel; + public boolean isBatteryCharging; + public String cellInfo; + public String cellRssi; + //wifi rssi + public int rssi; + public String ssid; + public String bssid; + public String wifiIpAddress; + // version of Mobilyzer + public String mobilyzerVersion; + // the set of apps on the device that are using Mobilyzer. + public ArrayList hostApps; + // the app which requests this measurement + public String requestApp; + + public String registrationId; + + public DeviceProperty(String deviceId, String appVersion, long timeStamp, + String osVersion, String ipConnectivity, String dnResolvability, + double longtitude, double latitude, String locationType, + String networkType, String carrier, String countryCode, int batteryLevel, boolean isCharging, + String cellInfo, String cellRssi, int rssi, String ssid, String bssid, String wifiIpAddress, + String mobilyzerVersion, HashSet hostApps, String requestApp) { + super(); + this.deviceId = deviceId; + this.appVersion = appVersion; + this.timestamp = timeStamp; + this.osVersion = osVersion; + this.ipConnectivity = ipConnectivity; + this.dnResolvability = dnResolvability; + this.location = new GeoLocation(longtitude, latitude); + this.locationType = locationType; + this.networkType = networkType; + this.carrier = carrier; + this.countryCode = countryCode; + this.batteryLevel = batteryLevel; + this.isBatteryCharging = isCharging; + this.cellInfo = cellInfo; + this.cellRssi = cellRssi; + this.rssi = rssi; + this.ssid = ssid; + this.bssid = bssid; + this.wifiIpAddress = wifiIpAddress; + this.mobilyzerVersion = mobilyzerVersion; + this.hostApps = new ArrayList(); + for ( String hostApp : hostApps ) { + this.hostApps.add(hostApp); + } + this.requestApp = requestApp; + } + + private DeviceProperty(Parcel in) { +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + deviceId = in.readString(); + appVersion = in.readString(); + timestamp = in.readLong(); + osVersion = in.readString(); + ipConnectivity = in.readString(); + dnResolvability = in.readString(); + location = in.readParcelable(GeoLocation.class.getClassLoader()); + locationType = in.readString(); + networkType = in.readString(); + carrier = in.readString(); + countryCode = in.readString(); + batteryLevel = in.readInt(); + isBatteryCharging = in.readByte() != 0; + cellInfo = in.readString(); + cellRssi = in.readString(); + rssi = in.readInt(); + ssid=in.readString(); + bssid=in.readString(); + wifiIpAddress=in.readString(); + mobilyzerVersion = in.readString(); + if(hostApps==null){ + hostApps = new ArrayList(); + } + in.readStringList(hostApps); + requestApp = in.readString(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public DeviceProperty createFromParcel(Parcel in) { + return new DeviceProperty(in); + } + + public DeviceProperty[] newArray(int size) { + return new DeviceProperty[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(deviceId); + dest.writeString(appVersion); + dest.writeLong(timestamp); + dest.writeString(osVersion); + dest.writeString(ipConnectivity); + dest.writeString(dnResolvability); + dest.writeParcelable(location, flags); + dest.writeString(locationType); + dest.writeString(networkType); + dest.writeString(carrier); + dest.writeString(countryCode); + dest.writeInt(batteryLevel); + dest.writeByte((byte) (isBatteryCharging ? 1 : 0)); + dest.writeString(cellInfo); + dest.writeString(cellRssi); + dest.writeInt(rssi); + dest.writeString(ssid); + dest.writeString(bssid); + dest.writeString(wifiIpAddress); + dest.writeString(mobilyzerVersion); + dest.writeStringList(hostApps); + dest.writeString(requestApp); + } + + public void setRegistrationId(String regid){//TODO temporarily fix + this.registrationId=regid; + } + + public String getLocation(){ + return this.location.toString(); + } + +} + +class GeoLocation implements Parcelable { + private double longitude; + private double latitude; + + public GeoLocation(double longtitude, double latitude) { + this.longitude = longtitude; + this.latitude = latitude; + } + + + private GeoLocation(Parcel in) { + longitude = in.readDouble(); + latitude = in.readDouble(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public GeoLocation createFromParcel(Parcel in) { + return new GeoLocation(in); + } + + public GeoLocation[] newArray(int size) { + return new GeoLocation[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeDouble(longitude); + dest.writeDouble(latitude); + } + + @Override + public String toString() { + return latitude+","+longitude; + } + +} diff --git a/src/com/mobilyzer/MeasurementDesc.java b/src/com/mobilyzer/MeasurementDesc.java new file mode 100644 index 0000000..fbae9e5 --- /dev/null +++ b/src/com/mobilyzer/MeasurementDesc.java @@ -0,0 +1,176 @@ +package com.mobilyzer; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * MeasurementDesc and all its subclasses are POJO classes that encode a measurement and enable easy + * (de)serialization. On the other hand {@link MeasurementTask} contains runtime specific + * information for task execution. + * + * @see MeasurementTask + */ +public abstract class MeasurementDesc implements Parcelable { + + public String type; + public String key; + public Date startTime; + public Date endTime; + public double intervalSec; + public long count; + public long priority; + public Map parameters; + public int contextIntervalSec; + + /** + * @param type Type of measurement (ping, dns, traceroute, etc.) that should execute this + * measurement task. + * @param startTime Earliest time that measurements can be taken using this Task descriptor. The + * current time will be used in place of a null startTime parameter. Measurements with a + * startTime more than 24 hours from now will NOT be run. + * @param endTime Latest time that measurements can be taken using this Task descriptor. Tasks + * with an endTime before startTime will be canceled. Corresponding to the 24-hour rule in + * startTime, tasks with endTime later than 24 hours from now will be assigned a new + * endTime that ends 24 hours from now. + * @param intervalSec Minimum number of seconds to elapse between consecutive measurements taken + * with this description. + * @param count Maximum number of times that a measurement should be taken with this description. + * A count of 0 means to continue the measurement indefinitely (until end_time). + * @param priority Larger values represent higher priorities. + * @param contextIntervalSec interval between the context collection + * @param params Measurement parameters. + */ + protected MeasurementDesc(String type, String key, Date startTime, Date endTime, + double intervalSec, long count, long priority, int contextIntervalSec, + Map params) { + super(); + this.type = type; + this.key = key; + if (startTime == null) { + this.startTime = Calendar.getInstance().getTime(); + } else { + this.startTime = new Date(startTime.getTime()); + } + long now = System.currentTimeMillis(); + if (endTime == null || endTime.getTime() - now > Config.TASK_EXPIRATION_MSEC) { + + this.endTime = new Date(now + Config.TASK_EXPIRATION_MSEC); + } else { + this.endTime = endTime; + } + if (intervalSec <= 0) { + this.intervalSec = Config.DEFAULT_SYSTEM_MEASUREMENT_INTERVAL_SEC; + } else { + this.intervalSec = intervalSec; + } + this.count = count; + this.priority = priority; + this.parameters = params; + + if (contextIntervalSec <= 0) { + this.contextIntervalSec = Config.DEFAULT_CONTEXT_INTERVAL_SEC; + } else { + this.contextIntervalSec = contextIntervalSec; + } + } + + /** Return the type of the measurement (DNS, Ping, Traceroute, etc.) */ + public abstract String getType(); + + /** Subclass override this method to initialize measurement specific parameters */ + protected abstract void initializeParams(Map params); + + @Override + public boolean equals(Object o) { + + MeasurementDesc another = (MeasurementDesc) o; + if (this.type.equals(another.type) && this.key.equals(another.key) + && this.startTime.equals(another.startTime) && this.endTime.equals(another.endTime) + && this.intervalSec == another.intervalSec && this.count == another.count + && this.priority == another.priority + && this.contextIntervalSec == another.contextIntervalSec +// && this.parameters.equals(another.parameters) + ) { + for (String key : this.parameters.keySet()) { + if (!this.parameters.get(key).equals(another.parameters.get(key))) { + return false; + } + } + + return true; + } + return false; + + } + + public void setParameters(Map newParams){ + this.parameters=newParams; + } + + public void setType(String type){ + this.type=type; + } + + @Override + public String toString() { + String result=type+","+key+","+intervalSec+","+count+","+priority+","+contextIntervalSec+","; + Object [] keys=parameters.keySet().toArray(); + Arrays.sort(keys); + for(Object k : keys){ + result+=parameters.get(k)+","; + } + return result; + } + + /** + * Necessary functions for Parcelable + * @param in Parcel object containing measurement descriptor + */ + protected MeasurementDesc(Parcel in) { +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + type = in.readString(); + key = in.readString(); + startTime = (Date) in.readSerializable(); + endTime = (Date) in.readSerializable(); + intervalSec = in.readDouble(); + count = in.readLong(); + priority = in.readLong(); + contextIntervalSec = in.readInt(); +// parameters = in.readHashMap(); + parameters=new HashMap(); + int parametersSize = in.readInt(); + + for (int i = 0; i < parametersSize; i++) { + parameters.put(in.readString(), in.readString()); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type); + dest.writeString(key); + dest.writeSerializable(startTime); + dest.writeSerializable(endTime); + dest.writeDouble(intervalSec); + dest.writeLong(count); + dest.writeLong(priority); + dest.writeInt(contextIntervalSec); +// dest.writeMap(parameters); + dest.writeInt(parameters.size()); + for (String s: parameters.keySet()) { + dest.writeString(s); + dest.writeString(parameters.get(s)); + } + } +} diff --git a/src/com/mobilyzer/MeasurementResult.java b/src/com/mobilyzer/MeasurementResult.java new file mode 100755 index 0000000..ade3355 --- /dev/null +++ b/src/com/mobilyzer/MeasurementResult.java @@ -0,0 +1,612 @@ +/* + * 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; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.HashMap; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.StringBuilderPrinter; + +import com.mobilyzer.measurements.DnsLookupTask; +import com.mobilyzer.measurements.HttpTask; +import com.mobilyzer.measurements.ParallelTask; +import com.mobilyzer.measurements.PingTask; +import com.mobilyzer.measurements.RRCTask; +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.PingTask.PingDesc; +import com.mobilyzer.measurements.TCPThroughputTask.TCPThroughputDesc; +import com.mobilyzer.measurements.TracerouteTask.TracerouteDesc; +import com.mobilyzer.measurements.UDPBurstTask.UDPBurstDesc; +import com.mobilyzer.measurements.VideoQoETask; +import com.mobilyzer.measurements.VideoQoETask.VideoQoEDesc; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.Util; + +/** + * POJO that represents the result of a measurement + * + * @see MeasurementDesc + */ +public class MeasurementResult implements Parcelable { + + private String deviceId; + private DeviceProperty properties;// TODO needed for sending back the + // results to server + private long timestamp; + private boolean success; + private String type; + private TaskProgress taskProgress; + private MeasurementDesc parameters; + private HashMap values; + private ArrayList> contextResults; + + public enum TaskProgress { + COMPLETED, PAUSED, FAILED, RESCHEDULED + } + + /** + * @param deviceProperty DeviceProperty object which will be attached to the result + * @param type measurement type + * @param timestamp + * @param taskProgress progress of the task: COMPLETED, PAUSED, FAILED + * @param measurementDesc MeasurementDesc of the task + */ + public MeasurementResult(String id, DeviceProperty deviceProperty, String type, long timeStamp, + TaskProgress taskProgress, MeasurementDesc measurementDesc) { + super(); + + this.deviceId = id; + this.type = type; + this.properties = deviceProperty; + this.timestamp = timeStamp; + this.taskProgress = taskProgress; + if (this.taskProgress == TaskProgress.COMPLETED) { + this.success = true; + } else { + this.success = false; + } + this.parameters = measurementDesc; + this.parameters.parameters = measurementDesc.parameters; + this.values = new HashMap(); + this.contextResults = new ArrayList>(); + } + + public MeasurementDesc getMeasurementDesc(){ + return this.parameters; + } + + public DeviceProperty getDeviceProperty(){ + return this.properties; + } + + + @SuppressWarnings("unchecked") + public void addContextResults(ArrayList> contextResults) { + this.contextResults = (ArrayList>) contextResults.clone(); + } + + private static String getStackTrace(Throwable error) { + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + error.printStackTrace(printWriter); + return result.toString(); + } + + /** + * Creates measurement result for the failed task by including the error message + * + * @param task input task that failed + * @param error that occurred during the execution of task + * @return list of failure measurement results for created for the input task + */ + public static MeasurementResult[] getFailureResult(MeasurementTask task, Throwable error) { + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + ArrayList results = new ArrayList(); + + if (task.getType().equals(ParallelTask.TYPE)) { + ParallelTask pTask = (ParallelTask) task; + MeasurementResult[] tempResults = + MeasurementResult.getFailureResults(pTask.getTasks(), error); + for (MeasurementResult r : tempResults) { + results.add(r); + } + } else if (task.getType().equals(SequentialTask.TYPE)) { + SequentialTask sTask = (SequentialTask) task; + MeasurementResult[] tempResults = + MeasurementResult.getFailureResults(sTask.getTasks(), error); + for (MeasurementResult r : tempResults) { + results.add(r); + } + } else { + MeasurementResult r = + new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(task.getKey()), task.getType(), + System.currentTimeMillis() * 1000, + TaskProgress.FAILED, task.measurementDesc); + Logger.e(error.toString() + "\n" + getStackTrace(error)); + r.addResult("error", error.toString()); + results.add(r); + } + return results.toArray(new MeasurementResult[results.size()]); + } + + /** + * Generating failure results for the task list in parallel and sequential + * tasks. Not public visible + * @param tasks task list in parallel and sequential tasks + * @param err error that occurred during the execution of task + * @return list of failure measurement results of the task list + */ + private static MeasurementResult[] getFailureResults(MeasurementTask[] tasks, Throwable err) { + ArrayList results = new ArrayList(); + if (tasks != null) { + for (MeasurementTask t : tasks) { + MeasurementResult[] tempResults = getFailureResult(t, err); + for (MeasurementResult r : tempResults) { + results.add(r); + } + } + } + return results.toArray(new MeasurementResult[results.size()]); + } + + /** + * Returns the type of this result + */ + public String getType() { + return parameters.getType(); + } + + /** + * @return Task progress for this task + */ + public TaskProgress getTaskProgress() { + return this.taskProgress; + } + + /** + * @param progress new task progress to be set + */ + public void setTaskProgress(TaskProgress progress) { + this.taskProgress = progress; + } + + /** + * @return key/value pairs of measurement result + */ + public HashMap getValues(){ + return this.values; + } + + /** + * Check if this task is succeed + * @return true if taskProgress equals to COMPLETED, false otherwise + */ + public boolean isSucceed() { + return this.taskProgress == TaskProgress.COMPLETED ? true : false; + } + + /** + * adds a new task parameter to the list of parameters + * @param key key for new parameter + * @param value value for new parameter + */ + public void setParameter(String key, String value) { + this.parameters.parameters.put(key, value); + } + + /** + * @param key key for retrieving value + * @return value for that key + */ + public String getParameter(String key) { + return this.parameters.parameters.get(key); + } + + public void setMeasurmentDesc(MeasurementDesc desc){ + this.parameters=desc; + } + + /* Add the measurement results of type String into the class */ + public void addResult(String resultType, Object resultVal) { + this.values.put(resultType, MeasurementJsonConvertor.toJsonString(resultVal)); + } + + /* Returns a string representation of the result */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + StringBuilderPrinter printer = new StringBuilderPrinter(builder); + Formatter format = new Formatter(); + try { + if (type.equals(PingTask.TYPE)) { + getPingResult(printer, values); + } else if (type.equals(HttpTask.TYPE)) { + getHttpResult(printer, values); + } else if (type.equals(DnsLookupTask.TYPE)) { + getDnsResult(printer, values); + } else if (type.equals(TracerouteTask.TYPE)) { + getTracerouteResult(printer, values); + } else if (type.equals(UDPBurstTask.TYPE)) { + getUDPBurstResult(printer, values); + } else if (type.equals(TCPThroughputTask.TYPE)) { + getTCPThroughputResult(printer, values); + } else if (type.equals(RRCTask.TYPE)){ + getRRCResult(printer, values); + } else if (type.equals(VideoQoETask.TYPE)) { + getVideoQoEResult(printer, values); + } + else { + Logger.e("Failed to get results for unknown measurement type " + type); + } + return builder.toString(); + } catch (NumberFormatException e) { + Logger.e("Exception occurs during constructing result string for user", e); + } catch (ClassCastException e) { + Logger.e("Exception occurs during constructing result string for user", e); + } catch (Exception e) { + Logger.e("Exception occurs during constructing result string for user", e); + } + return "Measurement has failed"; + } + + private void getPingResult(StringBuilderPrinter printer, HashMap values) { + PingDesc desc = (PingDesc) parameters; + printer.println("[Ping]"); + printer.println("Target: " + desc.target); + String ipAddress = removeQuotes(values.get("target_ip")); + // TODO: internationalize 'Unknown'. + if (ipAddress == null) { + ipAddress = "Unknown"; + } + printer.println("IP address: " + ipAddress); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + float packetLoss = Float.parseFloat(values.get("packet_loss")); + int count = Integer.parseInt(values.get("packets_sent")); + printer.println("\n" + count + " packets transmitted, " + (int) (count * (1 - packetLoss)) + + " received, " + (packetLoss * 100) + "% packet loss"); + + float value = Float.parseFloat(values.get("mean_rtt_ms")); + printer.println("Mean RTT: " + String.format("%.1f", value) + " ms"); + + value = Float.parseFloat(values.get("min_rtt_ms")); + printer.println("Min RTT: " + String.format("%.1f", value) + " ms"); + + value = Float.parseFloat(values.get("max_rtt_ms")); + printer.println("Max RTT: " + String.format("%.1f", value) + " ms"); + + value = Float.parseFloat(values.get("stddev_rtt_ms")); + printer.println("Std dev: " + String.format("%.1f", value) + " ms"); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("Ping paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + private void getHttpResult(StringBuilderPrinter printer, HashMap values) { + HttpDesc desc = (HttpDesc) parameters; + printer.println("[HTTP]"); + 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("Http paused!"); + } else { + printer.println("Http 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]"); + printer.println("Target: " + desc.target); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + String ipAddress = removeQuotes(values.get("address")); + if (ipAddress == null) { + ipAddress = "Unknown"; + } + printer.println("\nAddress: " + ipAddress); + int time = Integer.parseInt(values.get("time_ms")); + printer.println("Lookup time: " + time + " ms"); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("DNS look up paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + private void getTracerouteResult(StringBuilderPrinter printer, HashMap values) { + TracerouteDesc desc = (TracerouteDesc) parameters; + printer.println("[Traceroute]"); + printer.println("Target: " + desc.target); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + // Manually inject a new line + printer.println(" "); + + int hops = Integer.parseInt(values.get("num_hops")); + int hop_str_len = String.valueOf(hops + 1).length(); + for (int i = 1; i <= hops; i++) { + String key = "hop_" + i + "_addr_1"; + String ipAddress = removeQuotes(values.get(key)); + if (ipAddress == null) { + ipAddress = "Unknown"; + } + String hop_str = String.valueOf(i); + String hopInfo = hop_str; + for (int j = 0; j < hop_str_len + 1 - hop_str.length(); ++j) { + hopInfo += " "; + } + hopInfo += ipAddress; + // Maximum IP address length is 15. + for (int j = 0; j < 16 - ipAddress.length(); ++j) { + hopInfo += " "; + } + + key = "hop_" + i + "_rtt_ms"; + // The first and last character of this string are double + // quotes. + String timeStr = removeQuotes(values.get(key)); + if (timeStr == null) { + timeStr = "Unknown"; + printer.println(hopInfo + "-1 ms"); + }else{ + float time = Float.parseFloat(timeStr); + printer.println(hopInfo + String.format("%6.2f", time) + " ms"); + } + + + } + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("Traceroute paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + private void getUDPBurstResult(StringBuilderPrinter printer, HashMap values) { + UDPBurstDesc desc = (UDPBurstDesc) parameters; + if (desc.dirUp) { + printer.println("[UDPBurstUp]"); + } else { + printer.println("[UDPBurstDown]"); + } + printer.println("Target: " + desc.target); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + printer.println("IP addr: " + values.get("target_ip")); + printIPTestResult(printer); +// printer.println("Packet size: " + desc.packetSizeByte + "B"); +// printer.println("Number of packets to be sent: " + desc.udpBurstCount); +// printer.println("Interval between packets: " + desc.udpInterval + "ms"); + + String lossRatio = String.format("%.2f", Double.parseDouble(values.get("loss_ratio")) * 100); + String outOfOrderRatio = + String.format("%.2f", Double.parseDouble(values.get("out_of_order_ratio")) * 100); + printer.println("\nLoss ratio: " + lossRatio + "%"); + printer.println("Out of order ratio: " + outOfOrderRatio + "%"); + printer.println("Jitter: " + values.get("jitter") + "ms"); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("UDP Burst paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + private void getTCPThroughputResult(StringBuilderPrinter printer, HashMap values) { + TCPThroughputDesc desc = (TCPThroughputDesc) parameters; + if (desc.dir_up) { + printer.println("[TCP Uplink]"); + } else { + printer.println("[TCP Downlink]"); + } + printer.println("Target: " + desc.target); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + printer.println(""); + // Display result with precision up to 2 digit + String speedInJSON = values.get("tcp_speed_results"); + String dataLimitExceedInJSON = values.get("data_limit_exceeded"); + String displayResult = ""; + + double tp = desc.calMedianSpeedFromTCPThroughputOutput(speedInJSON); + double KB = Math.pow(2, 10); + if (tp < 0) { + displayResult = "No results available."; + } else if (tp > KB * KB) { + displayResult = "Speed: " + String.format("%.2f", tp / (KB * KB)) + " Gbps"; + } else if (tp > KB) { + displayResult = "Speed: " + String.format("%.2f", tp / KB) + " Mbps"; + } else { + displayResult = "Speed: " + String.format("%.2f", tp) + " Kbps"; + } + + // Append notice for exceeding data limit + if (dataLimitExceedInJSON.equals("true")) { + displayResult += + "\n* Task finishes earlier due to exceeding " + "maximum number of " + + ((desc.dir_up) ? "transmitted" : "received") + " bytes"; + } + printer.println(displayResult); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("TCP Throughput paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + + private void getRRCResult(StringBuilderPrinter printer, HashMap values) { + printer.println("[RRC Inference]"); + if (taskProgress == TaskProgress.COMPLETED) { + printer.println("Succeed!"); + } + else { + printer.println("Failed!"); + } + printer.println("Results uploaded to server"); + } + private void getVideoQoEResult(StringBuilderPrinter printer, HashMap values) { + VideoQoEDesc desc = (VideoQoEDesc) parameters; + printer.println("[Video QoE Measurement]"); + printer.println("Content ID: " + desc.contentId); + printer.println("Streaming Algorithm: " + desc.contentType); + printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp)); + printIPTestResult(printer); + + if (taskProgress == TaskProgress.COMPLETED) { + printer.println(""); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED)); + printer.println("Num of frame dropped" + ": " + values.get("video_num_frame_dropped")); + printer.println("Initial loading time" + ": " + values.get("video_initial_loading_time")); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME)); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP)); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE)); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP)); +// printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE)); + } else if (taskProgress == TaskProgress.PAUSED) { + printer.println("Video QoE task paused!"); + } else { + printer.println("Error: " + values.get("error")); + } + } + + /** + * Removes the quotes surrounding the string. If |str| is null, returns null. + */ + private String removeQuotes(String str) { + return str != null ? str.replaceAll("^\"|\"", "") : null; + } + + /** + * Print ip connectivity and hostname resolvability result + */ + private void printIPTestResult(StringBuilderPrinter printer) { +// printer.println("IPv4/IPv6 Connectivity: " + properties.ipConnectivity); +// printer.println("IPv4/IPv6 Domain Name Resolvability: " + properties.dnResolvability); + } + + /** Necessary function for Parcelable **/ + private MeasurementResult(Parcel in) { +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + deviceId = in.readString(); + properties = in.readParcelable(DeviceProperty.class.getClassLoader()); + timestamp = in.readLong(); + type = in.readString(); + taskProgress = (TaskProgress) in.readSerializable(); + if (this.taskProgress == TaskProgress.COMPLETED) { + this.success = true; + } else { + this.success = false; + } + parameters = in.readParcelable(MeasurementDesc.class.getClassLoader()); +// values = in.readHashMap(loader); + int valuesSize = in.readInt(); + values = new HashMap(); + for (int i = 0; i < valuesSize; i++) { + values.put(in.readString(), in.readString()); + } +// contextResults = in.readArrayList(loader); + contextResults= new ArrayList>(); + int contextResultsSize=in.readInt(); + for (int i = 0; i < contextResultsSize; i++) { + int contextResultsHashMapSize=in.readInt(); + HashMap tempHashMap= new HashMap(); + for (int j = 0; j < contextResultsHashMapSize; j++) { + tempHashMap.put(in.readString(), in.readString()); + } + contextResults.add(tempHashMap); + } + + + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public MeasurementResult createFromParcel(Parcel in) { + return new MeasurementResult(in); + } + + public MeasurementResult[] newArray(int size) { + return new MeasurementResult[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeString(deviceId); + out.writeParcelable(properties, flag); + out.writeLong(timestamp); + out.writeString(type); + out.writeSerializable(taskProgress); + out.writeParcelable(parameters, flag); +// out.writeMap(values); + out.writeInt(values.size()); + for (String s: values.keySet()) { + out.writeString(s); + out.writeString(values.get(s)); + } +// out.writeList(contextResults); + out.writeInt(contextResults.size()); + for (HashMap map: contextResults) { + out.writeInt(map.size()); + for(String s: map.keySet()){ + out.writeString(s); + out.writeString(map.get(s)); + } + } + + + } +} diff --git a/src/com/mobilyzer/MeasurementScheduler.java b/src/com/mobilyzer/MeasurementScheduler.java new file mode 100644 index 0000000..53b3b56 --- /dev/null +++ b/src/com/mobilyzer/MeasurementScheduler.java @@ -0,0 +1,1393 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Calendar; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Vector; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.PriorityBlockingQueue; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.gcm.GCMManager; +import com.mobilyzer.measurements.PageLoadTimeTask; +import com.mobilyzer.measurements.ParallelTask; +import com.mobilyzer.measurements.RRCTask; +import com.mobilyzer.measurements.SequentialTask; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.MeasurementJsonConvertor; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.IBinder; +import android.os.Messenger; +import android.os.Parcelable; +import android.preference.PreferenceManager; + +/** + * + * @author Ashkan Nikravesh (ashnik@umich.edu) + others The single scheduler thread that monitors + * the task queue, runs tasks at their specified times, and finally retrieves and reports + * results once they finish. The API can call the public methods or this Service. This + * service works as a remote service and always, we will have a single instance of scheduler + * running on a device, although we can have more than one app that binds to this service + * and communicate with that. + */ +public class MeasurementScheduler extends Service { + + public enum TaskStatus { + FINISHED, PAUSED, CANCELLED, SCHEDULED, RUNNING, NOTFOUND + } + + public enum DataUsageProfile { + PROFILE1, PROFILE2, PROFILE3, PROFILE4, UNLIMITED, NOTASSIGNED + } + + private ExecutorService measurementExecutor; + private BroadcastReceiver broadcastReceiver; + public boolean isSchedulerStarted = false; + + + public Checkin checkin;// TODO: should not be public, quick fix to get gcm working + private long checkinIntervalSec; + private long checkinRetryIntervalSec; + private int checkinRetryCnt; + private CheckinTask checkinTask; + private Calendar lastCheckinTime; + + private int batteryThreshold; + // private int dataLimit;//in Byte + private DataUsageProfile dataUsageProfile; + + + private PhoneUtils phoneUtils; + + private PendingIntent measurementIntentSender; + private PendingIntent checkinIntentSender; + private PendingIntent checkinRetryIntentSender; + + private volatile HashMap serverTasks; + // private volatile HashMap currentSchedule; + + private AlarmManager alarmManager; + private ResourceCapManager resourceCapManager; + private volatile ConcurrentHashMap tasksStatus; + // all the tasks are put in to this queue first, where they ordered based on their start time + private volatile PriorityBlockingQueue mainQueue; + // ready queue, all the tasks in this queue are ready to be run. They sorted based on + // (1) priority (2) end time + private volatile PriorityBlockingQueue waitingTasksQueue; + private volatile ConcurrentHashMap> pendingTasks; + private volatile Date currentTaskStartTime; + private volatile MeasurementTask currentTask; + + private volatile ConcurrentHashMap idToClientKey; + + private Messenger messenger; + + private GCMManager gcmManager; + + + @Override + public void onCreate() { + Logger.d("MeasurementScheduler -> onCreate called"); + PhoneUtils.setGlobalContext(this.getApplicationContext()); + + + + phoneUtils = PhoneUtils.getPhoneUtils(); + phoneUtils.registerSignalStrengthListener(); + + this.measurementExecutor = Executors.newSingleThreadExecutor(); + this.mainQueue = + new PriorityBlockingQueue(Config.MAX_TASK_QUEUE_SIZE, new TaskComparator()); + this.waitingTasksQueue = + new PriorityBlockingQueue(Config.MAX_TASK_QUEUE_SIZE, + new WaitingTasksComparator()); + this.pendingTasks = new ConcurrentHashMap>(); + this.tasksStatus = new ConcurrentHashMap(); + this.alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + this.resourceCapManager = new ResourceCapManager(Config.DEFAULT_BATTERY_THRESH_PRECENT, this); + + this.serverTasks = new HashMap(); + // this.currentSchedule = new HashMap(); + + this.idToClientKey = new ConcurrentHashMap(); + + messenger = new Messenger(new APIRequestHandler(this)); + + gcmManager = new GCMManager(this.getApplicationContext()); + + this.setCurrentTask(null); + this.setCurrentTaskStartTime(null); + + + this.checkin = new Checkin(this); + this.checkinRetryIntervalSec = Config.MIN_CHECKIN_RETRY_INTERVAL_SEC; + this.checkinRetryCnt = 0; + this.checkinTask = new CheckinTask(); + + this.batteryThreshold = -1; + this.checkinIntervalSec = -1; + this.dataUsageProfile = DataUsageProfile.NOTASSIGNED; + + +// loadSchedulerState();//TODO(ASHKAN) + + + + // Register activity specific BroadcastReceiver here + IntentFilter filter = new IntentFilter(); + filter.addAction(UpdateIntent.CHECKIN_ACTION); + filter.addAction(UpdateIntent.CHECKIN_RETRY_ACTION); + filter.addAction(UpdateIntent.MEASUREMENT_ACTION); + filter.addAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + filter.addAction(UpdateIntent.GCM_MEASUREMENT_ACTION); + filter.addAction(UpdateIntent.PLT_MEASUREMENT_ACTION); + //added by Clarence + filter.addAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + + + broadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + Logger.d(intent.getAction() + " RECEIVED"); + if (intent.getAction().equals(UpdateIntent.MEASUREMENT_ACTION)) { + handleMeasurement(); + + } else if (intent.getAction().equals(UpdateIntent.GCM_MEASUREMENT_ACTION)) { + try { + JSONObject json = + new JSONObject(intent.getExtras().getString(UpdateIntent.MEASUREMENT_TASK_PAYLOAD)); + Logger.d("MeasurementScheduler -> GCMManager: json task Value is " + json); + if (json != null && MeasurementTask.getMeasurementTypes().contains(json.get("type"))) { + + try { + MeasurementTask task = MeasurementJsonConvertor.makeMeasurementTaskFromJson(json); + task.getDescription().priority = MeasurementTask.GCM_PRIORITY; + task.getDescription().startTime = new Date(System.currentTimeMillis() - 1000); + task.getDescription().endTime = new Date(System.currentTimeMillis() + (600 * 1000)); + task.generateTaskID(); + task.getDescription().key = Config.SERVER_TASK_CLIENT_KEY; + submitTask(task); + } catch (IllegalArgumentException e) { + Logger.w("MeasurementScheduler -> GCM : Could not create task from JSON: " + e); + } + } + + } catch (JSONException e) { + Logger + .e("MeasurementSchedule -> GCMManager : Got exception during converting GCM json to MeasurementTask", + e); + } + + + } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION)) { + String taskKey = intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD); + String taskid = intent.getStringExtra(UpdateIntent.TASKID_PAYLOAD); + int priority = + intent.getIntExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.INVALID_PRIORITY); + + Logger.e(intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD) + " " + taskid + " " + + taskKey); + if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals(Config.TASK_FINISHED)) { + tasksStatus.put(taskid, TaskStatus.FINISHED); + Parcelable[] results = intent.getParcelableArrayExtra(UpdateIntent.RESULT_PAYLOAD); + if (results != null && results.length!=0) { + sendResultToClient(results, priority, taskKey, taskid); + Logger.i("Sending results to client..."); +// if(intent.hasExtra(UpdateIntent.TASK_TYPE_PAYLOAD) && intent.hasExtra(UpdateIntent.TASK_DESC_PAYLOAD) && +// (intent.getStringExtra(UpdateIntent.TASK_TYPE_PAYLOAD).equals(SequentialTask.TYPE) || +// intent.getStringExtra(UpdateIntent.TASK_TYPE_PAYLOAD).equals(ParallelTask.TYPE))){ +// boolean allSucceed=true; +// for (Object obj : results) { +// MeasurementResult r = (MeasurementResult) obj; +// allSucceed=allSucceed&(r.isSucceed()); +// } +// +// MeasurementDesc seq_desc=(MeasurementDesc)intent.getParcelableExtra(UpdateIntent.TASK_DESC_PAYLOAD); +// +// MeasurementResult combinedResults=new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, +// ((MeasurementResult)results[0]).getDeviceProperty() , intent.getStringExtra(UpdateIntent.TASK_TYPE_PAYLOAD), System.currentTimeMillis() * 1000, +// allSucceed? TaskProgress.COMPLETED: TaskProgress.FAILED, seq_desc); +// +// for (int i=0;i value : ((MeasurementResult)results[i]).getValues().entrySet()){ +// combinedResults.addResult("value_"+i+"_"+value.getKey(), value.getValue()); +// } +// combinedResults.addResult("type_"+i, ((MeasurementResult)results[i]).getType() ); +// for (String param : ((MeasurementResult)results[i]).getMeasurementDesc().parameters.keySet()){ +// combinedResults.addResult("param_"+i+"_"+param, ((MeasurementResult)results[i]).getMeasurementDesc().parameters.get(param)); +// } +// } +// combinedResults.addResult("num_results", results.length); +// combinedResults.getMeasurementDesc().parameters=null; +// Logger.d("# of results: "+results.length); +// +// if(results.length==2){ +// try { +// checkin.uploadSingleMeasurementResult(combinedResults, resourceCapManager); +// } catch (Exception e) { +// Logger.e("Failed to upload local event: "+e.getMessage()); +// } +// +// Parcelable[] newArray= new Parcelable[0]; +// results=newArray; +// } +// else{ +// Parcelable[] newArray= new Parcelable[1]; +// newArray[0]=combinedResults; +// results=newArray; +// } +// +// +// +// +// } + + + for (Object obj : results) { + try { + MeasurementResult result = (MeasurementResult) obj; + /** + * Nullify the additional parameters in MeasurmentDesc, or the results won't be + * accepted by GAE server + */ + result.getMeasurementDesc().parameters = null; + result.getDeviceProperty().registrationId=checkin.gcm_registraion_id; + Logger.d("REG ID: "+checkin.gcm_registraion_id); + String jsonResult = MeasurementJsonConvertor.encodeToJson(result).toString(); + saveResultToFile(jsonResult); + } catch (JSONException e) { + Logger.e("Error converting results to json format", e); + } + } + } + handleMeasurement(); + } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( + Config.TASK_PAUSED)) { + tasksStatus.put(taskid, TaskStatus.PAUSED); + } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( + Config.TASK_STOPPED)) { + tasksStatus.put(taskid, TaskStatus.SCHEDULED); + } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( + Config.TASK_CANCELED)) { + tasksStatus.put(taskid, TaskStatus.CANCELLED); + Parcelable[] results = intent.getParcelableArrayExtra(UpdateIntent.RESULT_PAYLOAD); + if (results != null) { + sendResultToClient(results, priority, taskKey, taskid); + } + } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( + Config.TASK_STARTED)) { + tasksStatus.put(taskid, TaskStatus.RUNNING); + } else if (intent.getStringExtra(UpdateIntent.TASK_STATUS_PAYLOAD).equals( + Config.TASK_RESUMED)) { + tasksStatus.put(taskid, TaskStatus.RUNNING); + } //added by Clarence + } else if (intent.getAction().equals(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION)){ + String IM_taskKey = intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD); + String IM_taskid = intent.getStringExtra(UpdateIntent.TASKID_PAYLOAD); + int IM_priority = + intent.getIntExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.INVALID_PRIORITY); + Parcelable[] IM_Results = intent.getParcelableArrayExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD); + if (IM_Results != null && IM_Results.length!=0) { + sendResultToClient(IM_Results, IM_priority, IM_taskKey, IM_taskid); + Logger.i("Sending Intermediate results to client..."); + System.out.println("receive the intermediate results"); + } + + } else if (intent.getAction().equals(UpdateIntent.CHECKIN_ACTION) + || intent.getAction().equals(UpdateIntent.CHECKIN_RETRY_ACTION)) { + Logger.d("Checkin intent received"); + handleCheckin(); + } + } + }; + this.registerReceiver(broadcastReceiver, filter); + } + + /** + * Save the results of a task to a file, for later uploading. This way, if the application + * crashes, is halted, etc. between the task and checkin, no results are lost. + * + * @param result The JSON representation of a result, as a string + */ + private synchronized void saveResultToFile(String result) { + try { + Logger.i("Saving result to file..."); + BufferedOutputStream writer = + new BufferedOutputStream(openFileOutput("results", Context.MODE_PRIVATE + | Context.MODE_APPEND)); + result += "\n"; + Logger.d("Measurement size in byte: "+result.length()); + writer.write(result.getBytes()); + writer.close(); + } catch (FileNotFoundException e) { + Logger.e("saveResultToFile->", e); + } catch (IOException e) { + Logger.e("saveResultToFile->", e); + } + } + + /* + * This callback ensures that we always use newest version scheduler on the device + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + Logger.e("MeasurementScheduler -> onStartCommand. Get start service intent from " + + intent.getStringExtra(UpdateIntent.CLIENTKEY_PAYLOAD) + " (API Ver " + + intent.getStringExtra(UpdateIntent.VERSION_PAYLOAD) + ")"); + /** + * Fetch the version in startService intent and stop itself if the version in the intent is + * higher than its own. + */ + int currentAPIVersion = Integer.parseInt(Config.version); + int newAPIVersion = Integer.parseInt(intent.getStringExtra(UpdateIntent.VERSION_PAYLOAD)); + + gcmManager.checkPlayServices(); + if (currentAPIVersion < newAPIVersion) { + Logger.e("Found scheduler version " + newAPIVersion + ", current version " + + currentAPIVersion); + Logger.e("Scheduler " + android.os.Process.myPid() + " stop itself"); + this.stopScheduler(); + return START_STICKY; + } + } + // Start up the thread running the service. + // Using one single thread for all requests + if (isSchedulerStarted == false) { + Logger.i("starting scheduler, load parameters from preference"); + isSchedulerStarted = true; + loadFromPreference(); + } + return START_STICKY; + } + + /** + * Load setting parameters from shared preference + */ + private void loadFromPreference() { + Logger.d("Scheduler loadFromPreference called"); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + boolean isForced = (PhoneUtils.clientKeySet.size() == 1); + + String currentBatteryThreshold = + prefs.getString(Config.PREF_KEY_BATTERY_THRESHOLD, + String.valueOf(Config.DEFAULT_BATTERY_THRESH_PRECENT)); + setBatteryThresh(isForced, Integer.parseInt(currentBatteryThreshold)); + + // API will always call startService before binding. So it is + // safe to set checkin interval here + String currentCheckinInterval = + prefs.getString(Config.PREF_KEY_CHECKIN_INTERVAL, + String.valueOf(Config.DEFAULT_CHECKIN_INTERVAL_SEC)); + setCheckinInterval(false, Long.parseLong(currentCheckinInterval)); + + // Fetch the data limit with 250 MB as a default + String currentProfile = + prefs.getString(Config.PREF_KEY_DATA_USAGE_PROFILE, DataUsageProfile.PROFILE3.name()); + this.setDataUsageLimit(isForced, DataUsageProfile.valueOf(currentProfile)); + + Logger.i("Preference set from SharedPreference: CheckinInterval=" + this.checkinIntervalSec + + ", Battery Threshold= " + this.batteryThreshold + ", Profile=" + + this.dataUsageProfile.name()); + } + + @Override + public IBinder onBind(Intent intent) { + return messenger.getBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + // All clients unbind by calling unbindService(). We can safely stop scheduler + Logger.e("All Clients unbind. Safe to stop now"); + this.stopScheduler(); + return false; + } + + @Override + public void onDestroy() { + Logger.d("MeasurementScheduler -> onDestroy"); + super.onDestroy(); + cleanUp(); + } + + // return the current running task + public synchronized MeasurementTask getCurrentTask() { + return currentTask; + } + + // set current running task + public synchronized void setCurrentTask(MeasurementTask newTask) { + if (newTask == null) { + currentTask = null; + Logger.d("Setting Current task -> null"); + } else { + Logger.d("Setting Current task: " + newTask.getTaskId()); + currentTask = newTask.clone(); + } + + } + + // set current running task start time + private synchronized void setCurrentTaskStartTime(Date starttime) { + currentTaskStartTime = starttime; + } + + // return the current running task (TODO synchronized?) + private synchronized Date getCurrentTaskStartTime() { + Date starttime; + starttime = currentTaskStartTime; + return starttime; + } + + + private synchronized void handleMeasurement() { + alarmManager.cancel(measurementIntentSender); + setCurrentTask(null); + try { + Logger.d("MeasurementScheduler -> In handleMeasurement " + mainQueue.size() + " " + + waitingTasksQueue.size()); + MeasurementTask task = mainQueue.peek(); + // update the waiting queue. It contains all the tasks that are ready + // to be executed. Here we extract all those ready tasks from main queue + while (task != null && task.timeFromExecution() <= 0) { + mainQueue.poll(); + + if(task.getDescription().getType().equals(PageLoadTimeTask.TYPE) && Build.VERSION.SDK_INT <=Build.VERSION_CODES.JELLY_BEAN_MR2){ + Logger.i("MeasurementScheduler: handleMeasurement: PageLoadTime task is only availabe on API level 19 and higher"); + task=mainQueue.peek(); + continue; + } + if(task.getDescription().getType().equals(RRCTask.TYPE) && phoneUtils.getNetwork().equals(PhoneUtils.NETWORK_WIFI)){ + long updatedStartTime = System.currentTimeMillis() + (long) (10 * 60 * 1000); + task.getDescription().startTime.setTime(updatedStartTime); + mainQueue.add(task); + Logger.i("MeasurementScheduler: handleMeasurement: delaying RRC task on "+phoneUtils.getNetwork()); + task = mainQueue.peek(); + continue; + } + Logger.i("MeasurementScheduler: handleMeasurement: "+task.getDescription().key + " " + task.getDescription().type + + " added to waiting list"); + waitingTasksQueue.add(task); + task = mainQueue.peek(); + } + + + if(!phoneUtils.isNetworkAvailable()){ + Logger.i("No connection is available, set an alarm for 5 min"); + measurementIntentSender = + PendingIntent.getBroadcast(this, 0, + new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), + PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (5*60*1000), + measurementIntentSender); + return; + } + + if (waitingTasksQueue.size() != 0) { + Logger.i("waiting list size is " + waitingTasksQueue.size()); + MeasurementTask ready = waitingTasksQueue.poll(); + Logger.i("ready: " + ready.getDescription().getType()); + + MeasurementDesc desc = ready.getDescription(); + long newStartTime = desc.startTime.getTime() + (long) desc.intervalSec * 1000; + + if (desc.count == MeasurementTask.INFINITE_COUNT + && desc.priority != MeasurementTask.USER_PRIORITY) { + if (serverTasks.containsKey(desc.toString()) + && serverTasks.get(desc.toString()).after(desc.endTime)) { + ready.getDescription().endTime.setTime(serverTasks.get(desc.toString()).getTime()); + } + } + + + /** + * Add a clone of the task if it's still valid it does not change the taskID (hashCode) + */ + if (newStartTime < ready.getDescription().endTime.getTime() + && (desc.count == MeasurementTask.INFINITE_COUNT || desc.count > 1)) { + MeasurementTask newTask = ready.clone(); + if (desc.count != MeasurementTask.INFINITE_COUNT) { + newTask.getDescription().count--; + } + newTask.getDescription().startTime.setTime(newStartTime); + tasksStatus.put(newTask.getTaskId(), TaskStatus.SCHEDULED); + mainQueue.add(newTask); + } else { + if (desc.priority != MeasurementTask.USER_PRIORITY) { + serverTasks.remove(desc.toString()); + } + } + + if (ready.getDescription().endTime.before(new Date())) { + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_CANCELED); + MeasurementResult[] tempResults = + MeasurementResult.getFailureResult(ready, + new CancellationException("Task cancelled!")); + intent.putExtra(UpdateIntent.RESULT_PAYLOAD, tempResults); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, ready.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, ready.getKey()); + MeasurementScheduler.this.sendBroadcast(intent); + + if (desc.priority != MeasurementTask.USER_PRIORITY) { + serverTasks.remove(desc.toString()); + } + + + handleMeasurement(); + } else { + Logger.d("MeasurementScheduler -> " + ready.getDescription().getType() + " is gonna run"); + Future future; + setCurrentTask(ready); + setCurrentTaskStartTime(Calendar.getInstance().getTime()); + if (ready.getDescription().priority == MeasurementTask.USER_PRIORITY) { + // User task can override the power policy. So a different task wrapper is used. + future = measurementExecutor.submit(new UserMeasurementTask(ready, this)); + } else { + future = + measurementExecutor.submit(new ServerMeasurementTask(ready, this, + resourceCapManager)); + } + + synchronized (pendingTasks) { + pendingTasks.put(ready, future); + } + } + } else {// if(task.timeFromExecution()>0){ + MeasurementTask waiting = mainQueue.peek(); + if (waiting != null) { + long timeFromExecution = task.timeFromExecution(); + measurementIntentSender = + PendingIntent.getBroadcast(this, 0, + new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), + PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution, + measurementIntentSender); + setCurrentTask(null);// TODO + setCurrentTaskStartTime(null); + } + + } + } catch (IllegalArgumentException e) { + // Task creation in clone can create this exception + + } catch (Exception e) { + // We don't want any unexpected exception to crash the process + } + } + + // returns taskId on success submissions + public synchronized String submitTask(MeasurementTask newTask) { + // TODO check if scheduler is running... + // and there is a current running/scheduled task + String newTaskId = newTask.getTaskId(); + tasksStatus.put(newTaskId, TaskStatus.SCHEDULED); + idToClientKey.put(newTaskId, newTask.getKey()); + Logger.d("MeasurementScheduler --> submitTask: " + newTask.getDescription().key + " " + + newTaskId); + MeasurementTask current; + if (getCurrentTask() != null) { + current = getCurrentTask(); + Logger.d("submitTask: current is NOT null"); + } else { + current = null; + Logger.d("submitTask: current is null"); + } + // preemption condition + if (current != null + && newTask.getDescription().priority < current.getDescription().priority + && new Date(current.getDuration() + getCurrentTaskStartTime().getTime()).after(newTask + .getDescription().endTime)) { + Logger.d("submitTask: trying to cancel/preempt the task"); + // finding the current instance in pending tasks. we can call + // pause on that instance only + if (pendingTasks.containsKey(current)) { + for (MeasurementTask mt : pendingTasks.keySet()) { + if (current.equals(mt)) { + current = mt; + break; + } + } + Logger.e("Cancelling Current Task"); + if (current instanceof PreemptibleMeasurementTask + && ((PreemptibleMeasurementTask) current).pause()) { + pendingTasks.remove(current); + ((PreemptibleMeasurementTask) current).updateTotalRunningTime(System.currentTimeMillis() + - getCurrentTaskStartTime().getTime()); + if (newTask.timeFromExecution() <= 0) { + mainQueue.add(newTask); + mainQueue.add(current); + handleMeasurement(); + } else { + mainQueue.add(newTask); + mainQueue.add(current); + long timeFromExecution = newTask.timeFromExecution(); + measurementIntentSender = + PendingIntent.getBroadcast(this, 0, new UpdateIntent( + UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + timeFromExecution, measurementIntentSender); + setCurrentTask(newTask); + setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution)); + } + + } else if (current.stop()) { + pendingTasks.remove(current); + if (newTask.timeFromExecution() <= 0) { + mainQueue.add(newTask); + mainQueue.add(current); + handleMeasurement(); + } else { + mainQueue.add(newTask); + mainQueue.add(current); + long timeFromExecution = newTask.timeFromExecution(); + measurementIntentSender = + PendingIntent.getBroadcast(this, 0, new UpdateIntent( + UpdateIntent.MEASUREMENT_ACTION), PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + timeFromExecution, measurementIntentSender); + // setCurrentTask(null); + setCurrentTask(newTask); + setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution)); + } + } else { + mainQueue.add(newTask); + } + } else { + alarmManager.cancel(measurementIntentSender); + if (newTask.timeFromExecution() <= 0) { + mainQueue.add(newTask); + handleMeasurement(); + } else { + mainQueue.add(newTask); + long timeFromExecution = newTask.timeFromExecution(); + measurementIntentSender = + PendingIntent.getBroadcast(this, 0, + new UpdateIntent(UpdateIntent.MEASUREMENT_ACTION), + PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeFromExecution, + measurementIntentSender); + // setCurrentTask(null); + setCurrentTask(newTask); + setCurrentTaskStartTime(new Date(System.currentTimeMillis() + timeFromExecution)); + } + } + } else { + Logger.d("submitTask: adding to mainqueue"); + mainQueue.add(newTask); + if (current == null) { + Logger.d("submitTask: adding to mainqueue, current is null"); + Logger.d("submitTask: calling handleMeasurement"); + alarmManager.cancel(measurementIntentSender); + handleMeasurement(); + } else { + Logger.d("submitTask: adding to mainqueue, current is not null: " + + current.getMeasurementType() + " " + getCurrentTaskStartTime()); + if (pendingTasks.containsKey(current)) { + if (pendingTasks.get(current).isDone()) { + alarmManager.cancel(measurementIntentSender); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 3 * 1000, + measurementIntentSender); + } else if (getCurrentTaskStartTime() != null) { + if (!current.getMeasurementType().equals(RRCTask.TYPE) + && new Date(System.currentTimeMillis() - Config.MAX_TASK_DURATION) + .after(getCurrentTaskStartTime())) { + pendingTasks.get(current).cancel(true); + handleMeasurement(); + + } else if (current.getMeasurementType().equals(RRCTask.TYPE) + && new Date(System.currentTimeMillis() + - (Config.DEFAULT_RRC_TASK_DURATION + 15 * 60 * 1000)) + .after(getCurrentTaskStartTime())) { + pendingTasks.get(current).cancel(true); + handleMeasurement(); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + Config.MAX_TASK_DURATION / 2, measurementIntentSender); + } + } + + + } else { + Logger.d("submitTask: not found in pending task"); + handleMeasurement(); + } + + } + } + return newTaskId; + } + + public synchronized boolean cancelTask(String taskId, String clientKey) { + + if (clientKey.equals(Config.SERVER_TASK_CLIENT_KEY)) { + return false; + } + + Logger.i("Cancel task " + taskId + " from " + clientKey); + if (taskId != null && idToClientKey.containsKey(taskId)) { + if (idToClientKey.get(taskId).equals(clientKey)) { + boolean found = false; + for (Object object : mainQueue) { + MeasurementTask task = (MeasurementTask) object; + if (task.getTaskId().equals(taskId) && task.getKey().equals(clientKey)) { + mainQueue.remove(task); + found = true; + } + } + + for (Object object : waitingTasksQueue) { + MeasurementTask task = (MeasurementTask) object; + if (task.getTaskId().equals(taskId) && task.getKey().equals(clientKey)) { + waitingTasksQueue.remove(task); + found = true; + } + } + MeasurementTask currentMeasurementTask = getCurrentTask(); + if (currentMeasurementTask != null) { + Logger.i("submitTask: current taskId " + currentMeasurementTask.getTaskId()); + if (currentMeasurementTask.getTaskId().equals(taskId) + && currentMeasurementTask.getKey().equals(clientKey)) { + for (MeasurementTask mt : pendingTasks.keySet()) { + if (currentMeasurementTask.equals(mt)) { + currentMeasurementTask = mt; + break; + } + } + pendingTasks.remove(currentMeasurementTask); + return currentMeasurementTask.stop(); + } + } + + return found; + } + } + return false; + } + + private void persistParams(String key, String value) { + Logger.d("Scheduler persistParams called"); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(key, value); + editor.commit(); + } + + /** + * @param newBatteryThresh + * @return the final value of battery threshold + */ + // API should ensure that the value is between 0 and 100 + public synchronized int setBatteryThresh(boolean isForced, int newBatteryThresh) { + Logger.d("Scheduler->setBatteryThresh called"); + int min = Config.MIN_BATTERY_THRESHOLD; + int max = Config.MAX_BATTERY_THRESHOLD; + /** + * If there are multiple apps registered, we gonna be conservative Only allow raising the + * battery threshold + */ + if (!isForced && this.batteryThreshold != -1) { + min = this.batteryThreshold; + } + // Check whether out of boundary and same with old value + if (newBatteryThresh >= min && newBatteryThresh <= max + && newBatteryThresh != this.batteryThreshold) { + this.batteryThreshold = newBatteryThresh; + Logger.i("Setting battery threshold to " + this.batteryThreshold + "%"); + persistParams(Config.PREF_KEY_BATTERY_THRESHOLD, String.valueOf(this.batteryThreshold)); + } + this.resourceCapManager.setBatteryThresh(this.batteryThreshold); + return this.batteryThreshold; + } + + /** + * If the users have not specified the threshold, it will return the default value + * + * @return + */ + public synchronized int getBatteryThresh() { + if (this.batteryThreshold == -1) { + return Config.DEFAULT_BATTERY_THRESH_PRECENT; + } + return this.batteryThreshold; + } + + + /** Set the interval for checkin in seconds */ + public synchronized long setCheckinInterval(boolean isForced, long interval) { + Logger.d("Scheduler->setCheckinInterval called " + interval); + long min = Config.MIN_CHECKIN_INTERVAL_SEC; + long max = Config.MAX_CHECKIN_INTERVAL_SEC; + + + if (!isForced && this.checkinIntervalSec != -1) { + min = this.batteryThreshold; + } + // Check whether out of boundary and same with old value + if (interval >= min && interval <= max && interval != this.checkinIntervalSec) { + this.checkinIntervalSec = interval; + Logger.i("Setting checkin interval to " + this.checkinIntervalSec + " seconds"); + persistParams(Config.PREF_KEY_CHECKIN_INTERVAL, String.valueOf(this.checkinIntervalSec)); + // the new checkin schedule will start + // in PAUSE_BETWEEN_CHECKIN_CHANGE_MSEC seconds + checkinIntentSender = + PendingIntent.getBroadcast(this, 0, new UpdateIntent(UpdateIntent.CHECKIN_ACTION), + PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + Config.PAUSE_BETWEEN_CHECKIN_CHANGE_MSEC, checkinIntervalSec * 1000, + checkinIntentSender); + } + return this.checkinIntervalSec; + } + + /** Returns the checkin interval of the scheduler in seconds */ + public synchronized long getCheckinInterval() { + if (this.checkinIntervalSec == -1) { + return Config.DEFAULT_CHECKIN_INTERVAL_SEC; + } + return this.checkinIntervalSec; + } + + public synchronized void setDataUsageLimit(boolean isForced, DataUsageProfile profile) { + Logger.d("Scheduler->setDataUsageLimit called"); + int min = DataUsageProfile.PROFILE1.ordinal(); + int max = DataUsageProfile.UNLIMITED.ordinal(); + if (!isForced && profile != null && profile != DataUsageProfile.NOTASSIGNED) { + max = this.dataUsageProfile.ordinal(); + } + if (profile.ordinal() >= min && profile.ordinal() <= max && profile != this.dataUsageProfile) { + this.dataUsageProfile = profile; + resourceCapManager.setDataUsageLimit(profile); + Logger.i("Setting data usage profile to " + this.dataUsageProfile); + persistParams(Config.PREF_KEY_DATA_USAGE_PROFILE, this.dataUsageProfile.name()); + } + } + + public synchronized DataUsageProfile getDataUsageProfile() { + if (this.dataUsageProfile.equals(DataUsageProfile.NOTASSIGNED)) { + return DataUsageProfile.PROFILE3; + } + return this.dataUsageProfile; + } + + public synchronized void setAuthenticateAccount(String selectedAccount) { + Logger.d("Scheduler->setAuthenticateAccount called"); + if (selectedAccount != null) { + // Verify that the account is valid or is anonymous + String[] accounts = AccountSelector.getAccountList(getApplicationContext()); + for (String account : accounts) { + if (account.equals(selectedAccount)) { + Logger.i("Setting authenticate account to " + account); + persistParams(Config.PREF_KEY_SELECTED_ACCOUNT, account); + return; + } + } + Logger.e("Error: " + selectedAccount + " doesn't match any account or anonymous"); + } + } + + /** + * Get current authenticate account of Mobilyzer + * + * @return account in string if already set by clients(include Anonymous), or null if havn't been + * set yet(it will checkin as anonymous user) + */ + public synchronized String getAuthenticateAccount() { + Logger.d("Scheduler->getAuthenticateAccount called"); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + String selectedAccount = prefs.getString(Config.PREF_KEY_SELECTED_ACCOUNT, null); + Logger.i("Get authenticate account: " + selectedAccount); + return selectedAccount; + } + + /** + * Adjusts the frequency of the task based on the profile passed from the server. + * + * Alternately, disregards the task altogether, if a -1 is passed. + * + * @param task The task to adjust + * @return false if the task should not be scheduled, based on the profile + */ + private boolean adjustInterval(MeasurementTask task) { + + Map params = task.getDescription().parameters; + float adjust = 1; // default + if (params.containsKey("profile_1_freq") && getDataUsageProfile() == DataUsageProfile.PROFILE1) { + adjust = Float.parseFloat(params.get("profile_1_freq")); + Logger.i("Task " + task.getDescription().key + " adjusted using profile 1"); + } else if (params.containsKey("profile_2_freq") + && getDataUsageProfile() == DataUsageProfile.PROFILE2) { + adjust = Float.parseFloat(params.get("profile_2_freq")); + Logger.i("Task " + task.getDescription().key + " adjusted using profile 2"); + } else if (params.containsKey("profile_3_freq") + && getDataUsageProfile() == DataUsageProfile.PROFILE3) { + adjust = Float.parseFloat(params.get("profile_3_freq")); + Logger.i("Task " + task.getDescription().key + " adjusted using profile 3"); + } else if (params.containsKey("profile_4_freq") + && getDataUsageProfile() == DataUsageProfile.PROFILE4) { + adjust = Float.parseFloat(params.get("profile_4_freq")); + Logger.i("Task " + task.getDescription().key + " adjusted using profile 4"); + } else if (params.containsKey("profile_unlimited") + && getDataUsageProfile() == DataUsageProfile.UNLIMITED) { + adjust = Float.parseFloat(params.get("profile_unlimited")); + Logger.i("Task " + task.getDescription().key + " adjusted using unlimited profile"); + } + if (adjust <= 0) { + Logger.i("Task " + task.getDescription().key + "marked for removal"); + return false; + } + // if (!task.getType().equals(RRCTask.TYPE)) { + // return false; + // } + task.getDescription().intervalSec *= adjust; + Calendar now = Calendar.getInstance(); + now.add(Calendar.SECOND, (int) (task.getDescription().intervalSec / 24)); + task.getDescription().startTime = now.getTime(); + if (task.getDescription().startTime.after(task.getDescription().endTime)) { + task.getDescription().endTime = + new Date(task.getDescription().startTime.getTime() + Config.TASK_EXPIRATION_MSEC); + } + // Logger.d(task.getDescription().startTime+" "+task.getDescription().endTime); + return true; + + } + + public TaskStatus getTaskStatus(String taskID) { + if (tasksStatus.get(taskID) == null) { + return TaskStatus.NOTFOUND; + } + return tasksStatus.get(taskID); + } + + private class TaskComparator implements Comparator { + + @Override + public int compare(MeasurementTask task1, MeasurementTask task2) { + return task1.compareTo(task2); + } + } + + private class WaitingTasksComparator implements Comparator { + + @Override + public int compare(MeasurementTask task1, MeasurementTask task2) { + Long task1Prority = task1.measurementDesc.priority; + Long task2Priority = task2.measurementDesc.priority; + + int priorityComparison = task1Prority.compareTo(task2Priority); + if (priorityComparison == 0 && task1.measurementDesc.endTime != null + && task2.measurementDesc.endTime != null) { + return task1.measurementDesc.endTime.compareTo(task2.measurementDesc.endTime); + } else { + return priorityComparison; + } + } + } + + /** + * Send measurement results to the client that submit the task, or broadcast the result of server + * scheduled task to each connected clients + * + * @param results Measurement result to be sent + * @param priority Priority for the task. Determine the communication way - Unicast for user task, + * Broadcast for server task + * @param clientKey Client key for the task + * @param taskId Unique task id for the task + */ + public void sendResultToClient(Parcelable[] results, int priority, String clientKey, String taskId) { + Intent intent = new Intent(); + intent.putExtra(UpdateIntent.RESULT_PAYLOAD, results); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, taskId); + if (priority == MeasurementTask.USER_PRIORITY) { + Logger.d("Sending result to client " + clientKey + ": taskId " + taskId); + intent.setAction(UpdateIntent.USER_RESULT_ACTION + "." + clientKey); + } else { + Logger.d("Broadcasting result: taskId " + taskId); + intent.setAction(UpdateIntent.SERVER_RESULT_ACTION); + } + this.sendBroadcast(intent); + } + + /** + * Stop the scheduler + */ + public synchronized void stopScheduler() { + this.notifyAll(); + // this.stopForeground(true); + this.stopSelf(); + } + + private synchronized void cleanUp() { + Logger.d("Service cleanUp called"); + this.mainQueue.clear(); + this.waitingTasksQueue.clear(); + + if (this.currentTask != null) { + this.currentTask.stop(); + } + + // remove all future tasks + this.measurementExecutor.shutdown(); + // remove and stop all active tasks + this.measurementExecutor.shutdownNow(); + this.checkin.shutDown(); + + this.unregisterReceiver(broadcastReceiver); + Logger.d("canceling pending intents"); + + if (checkinIntentSender != null) { + checkinIntentSender.cancel(); + alarmManager.cancel(checkinIntentSender); + } + if (checkinRetryIntentSender != null) { + checkinRetryIntentSender.cancel(); + alarmManager.cancel(checkinRetryIntentSender); + } + if (measurementIntentSender != null) { + measurementIntentSender.cancel(); + alarmManager.cancel(measurementIntentSender); + } + this.notifyAll(); + phoneUtils.shutDown(); + + + + Logger.i("Shut down all executors and stopping service"); + } + + + private void getTasksFromServer() throws IOException { + Logger.i("Downloading tasks from the server"); + checkin.getCookie(); + List tasksFromServer = checkin.checkin(resourceCapManager, gcmManager); + // The new task schedule overrides the old one + + Logger.i("Received " + tasksFromServer.size() + " task(s) from server"); + + for (MeasurementTask task : tasksFromServer) { + task.measurementDesc.key = Config.SERVER_TASK_CLIENT_KEY; + if (adjustInterval(task)) { + if (task.getDescription().count == MeasurementTask.INFINITE_COUNT) { + if (!serverTasks.containsKey(task.getDescription().toString())) { + this.mainQueue.add(task); + } + serverTasks.put(task.getDescription().toString(), task.getDescription().endTime); + } else { + this.mainQueue.add(task); + } + } + } +// saveSchedulerState();//TODO(ASHKAN) + } + + /** + * Save the entire current schedule to a file, in JSON format, like how tasks are received from + * the server. + * + * One item per line. + */ + private void saveSchedulerState() { + + try { + BufferedOutputStream writer = + new BufferedOutputStream(openFileOutput("schedule", Context.MODE_PRIVATE)); + Object[] mainQueueArray = new Object[0]; + Logger.i("Saving schedule to a file..."); + synchronized (mainQueue) { + mainQueueArray = mainQueue.toArray(); + } + + for (Object entry : mainQueueArray) { + try { + JSONObject task = + MeasurementJsonConvertor.encodeToJson(((MeasurementTask) entry).getDescription()); + String taskstring = task.toString() + "\n"; + writer.write(taskstring.getBytes()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + Object[] waitingTasksArray = new Object[0]; + synchronized (pendingTasks) { + waitingTasksArray = waitingTasksQueue.toArray(); + } + + for (Object entry : waitingTasksArray) { + try { + JSONObject task = + MeasurementJsonConvertor.encodeToJson(((MeasurementTask) entry).getDescription()); + String taskstring = task.toString() + "\n"; + writer.write(taskstring.getBytes()); + } catch (JSONException e) { + e.printStackTrace(); + } + } + writer.close(); + } catch (FileNotFoundException e) { + Logger.e("saveSchedulerState->", e); + } catch (IOException e) { + Logger.e("saveSchedulerState->", e); + } + + } + + /** + * Load the schedule from the schedule file, if it exists. + * + * This is to be run when the app first starts up, so scheduled items are not lost. + */ + private void loadSchedulerState() { + // Vector tasksToAdd = new Vector(); + synchronized (mainQueue) { + try { + Logger.i("Restoring schedule from disk..."); + FileInputStream inputstream = openFileInput("schedule"); + InputStreamReader streamreader = new InputStreamReader(inputstream); + BufferedReader bufferedreader = new BufferedReader(streamreader); + + String line; + while ((line = bufferedreader.readLine()) != null) { + JSONObject jsonTask; + try { + jsonTask = new JSONObject(line); + MeasurementTask newTask = + MeasurementJsonConvertor.makeMeasurementTaskFromJson(jsonTask); + + // If the task is scheduled in the past, re-schedule it in the future + // We assume tasks in the past have run, otherwise we can wind up getting + // stuck trying to run a large backlog of tasks + + long curtime = System.currentTimeMillis(); + + if (curtime > newTask.getDescription().endTime.getTime()){ + continue; + } + + if (curtime > newTask.getDescription().startTime.getTime()) { + long timediff = curtime - newTask.getDescription().startTime.getTime(); + + timediff = (long) (timediff % (newTask.getDescription().intervalSec * 1000)); + Calendar now = Calendar.getInstance(); + now.add(Calendar.SECOND, (int) timediff / 1000); + newTask.getDescription().startTime.setTime(now.getTimeInMillis()); + Logger.i("Rescheduled task " + newTask.getDescription().key + " at time " + + now.getTimeInMillis()); + } + + mainQueue.add(newTask); + } catch (JSONException e) { + e.printStackTrace(); + } + } + handleMeasurement(); + bufferedreader.close(); + streamreader.close(); + inputstream.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + + + private void resetCheckin() { + // reset counters for checkin + checkinRetryCnt = 0; + checkinRetryIntervalSec = Config.MIN_CHECKIN_RETRY_INTERVAL_SEC; + checkin.initializeAccountSelector(); + } + + private class CheckinTask implements Runnable { + @Override + public void run() { + Logger.i("checking Speedometer service for new tasks"); + lastCheckinTime = Calendar.getInstance(); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + try { + uploadResults(); + getTasksFromServer(); + // Also reset checkin if we get a success + resetCheckin(); + // Schedule the new tasks + if (getCurrentTask() == null) {// TODO check this + alarmManager.cancel(measurementIntentSender); + handleMeasurement(); + } + // + } catch (Exception e) { + /* + * Executor stops all subsequent execution of a periodic task if a raised exception is + * uncaught. We catch all undeclared exceptions here + */ + Logger.e("Unexpected exceptions caught", e); + if (checkinRetryCnt > Config.MAX_CHECKIN_RETRY_COUNT) { + /* + * If we have retried more than MAX_CHECKIN_RETRY_COUNT times upon a checkin failure, we + * will stop retrying and wait until the next checkin period + */ + resetCheckin(); + } else if (checkinRetryIntervalSec < checkinIntervalSec) { + Logger.i("Retrying checkin in " + checkinRetryIntervalSec + " seconds"); + /* + * Use checkinRetryIntentSender so that the periodic checkin schedule will remain intact + */ + checkinRetryIntentSender = + PendingIntent.getBroadcast(MeasurementScheduler.this, 0, new UpdateIntent( + UpdateIntent.CHECKIN_RETRY_ACTION), PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + checkinRetryIntervalSec * 1000, checkinRetryIntentSender); + checkinRetryCnt++; + checkinRetryIntervalSec = + Math.min(Config.MAX_CHECKIN_RETRY_INTERVAL_SEC, checkinRetryIntervalSec * 2); + } + } finally { + if (phoneUtils != null) { + phoneUtils.releaseWakeLock(); + } + } + } + } + + private void uploadResults() { + Vector finishedTasks = new Vector(); + MeasurementResult[] results; + Future future; + Logger.d("pendingTasks: "+pendingTasks.size()); + synchronized (this.pendingTasks) { + try { + for (MeasurementTask task : this.pendingTasks.keySet()) { + future = this.pendingTasks.get(task); + if (future != null) { + if (future.isDone()) { + this.pendingTasks.remove(task); + if (future.isCancelled()) { + Logger.e("Task execution was canceled"); + results = + MeasurementResult.getFailureResult(task, new CancellationException( + "Task cancelled")); + for (MeasurementResult r : results) { + finishedTasks.add(r); + } + } + } + } + } + } catch (ConcurrentModificationException e) { + /* + * keySet is a synchronized view of the keys. However, changes during iteration will throw + * ConcurrentModificationException. Since we have synchronized all changes to pendingTasks + * this should not happen. + */ + Logger.e("Pending tasks is changed during measurement upload"); + } + } + Logger.i("A total of " + finishedTasks.size() + " from pendingTasks is uploaded"); + Logger.i("A total of " + this.pendingTasks.size() + " is in pendingTasks"); + + try { + for (MeasurementResult r : finishedTasks) { + r.getMeasurementDesc().parameters = null; + } + this.checkin.uploadMeasurementResult(finishedTasks, resourceCapManager); + } catch (IOException e) { + Logger.e("Error when uploading message"); + } + } + + + /** Returns the last checkin time */ + public synchronized Date getLastCheckinTime() { + if (lastCheckinTime != null) { + return lastCheckinTime.getTime(); + } else { + return null; + } + } + + /** Returns the next (expected) checkin time */ + public synchronized Date getNextCheckinTime() { + if (lastCheckinTime != null) { + Calendar nextCheckinTime = (Calendar) lastCheckinTime.clone(); + nextCheckinTime.add(Calendar.SECOND, (int) getCheckinInterval()); + return nextCheckinTime.getTime(); + } else { + return null; + } + } + + /** + * Perform a checkin operation. + */ + public void handleCheckin() { + // if ( PhoneUtils.getPhoneUtils().isCharging() == false + // && PhoneUtils.getPhoneUtils().getCurrentBatteryLevel() < getBatteryThresh()) { + if (!resourceCapManager.canScheduleExperiment()) { + Logger.e("Checkin skipped - below battery threshold " + getBatteryThresh()); + return; + } + /* + * The CPU can go back to sleep immediately after onReceive() returns. Acquire the wake lock for + * the new thread here and release the lock when the thread finishes + */ + PhoneUtils.getPhoneUtils().acquireWakeLock(); + new Thread(checkinTask).start(); + } +} diff --git a/src/com/mobilyzer/MeasurementTask.java b/src/com/mobilyzer/MeasurementTask.java new file mode 100644 index 0000000..b756378 --- /dev/null +++ b/src/com/mobilyzer/MeasurementTask.java @@ -0,0 +1,262 @@ +package com.mobilyzer; + + +import java.io.InvalidClassException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Set; +import java.util.concurrent.Callable; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.measurements.DnsLookupTask; +import com.mobilyzer.measurements.HttpTask; +import com.mobilyzer.measurements.PageLoadTimeTask; +import com.mobilyzer.measurements.PingTask; +import com.mobilyzer.measurements.RRCTask; +import com.mobilyzer.measurements.SequentialTask; +import com.mobilyzer.measurements.TCPThroughputTask; +import com.mobilyzer.measurements.TracerouteTask; +import com.mobilyzer.measurements.UDPBurstTask; +import com.mobilyzer.measurements.VideoQoETask; + + + +public abstract class MeasurementTask + implements + Callable, + Comparable, + Parcelable { + protected MeasurementDesc measurementDesc; + protected String taskId; + + //private MeasurementScheduler scheduler; // added by Clarence + private Context context = null; //added by Clarence + + + public static final int USER_PRIORITY = Integer.MIN_VALUE; + /* used for Server tasks */ + public static final int INVALID_PRIORITY = Integer.MAX_VALUE; + public static final int GCM_PRIORITY = 1234;//TODO just for testing + public static final int INFINITE_COUNT = -1; + + private static HashMap measurementTypes; + // Maps between the type of task and its readable name + private static HashMap measurementDescToType; + + static { + measurementTypes = new HashMap(); + measurementDescToType = new HashMap(); + measurementTypes.put(PingTask.TYPE, PingTask.class); + measurementDescToType.put(PingTask.DESCRIPTOR, PingTask.TYPE); + measurementTypes.put(HttpTask.TYPE, HttpTask.class); + measurementDescToType.put(HttpTask.DESCRIPTOR, HttpTask.TYPE); + measurementTypes.put(TracerouteTask.TYPE, TracerouteTask.class); + measurementDescToType.put(TracerouteTask.DESCRIPTOR, TracerouteTask.TYPE); + measurementTypes.put(DnsLookupTask.TYPE, DnsLookupTask.class); + measurementDescToType.put(DnsLookupTask.DESCRIPTOR, DnsLookupTask.TYPE); + measurementTypes.put(TCPThroughputTask.TYPE, TCPThroughputTask.class); + measurementDescToType.put(TCPThroughputTask.DESCRIPTOR, TCPThroughputTask.TYPE); + measurementTypes.put(UDPBurstTask.TYPE, UDPBurstTask.class); + measurementDescToType.put(UDPBurstTask.DESCRIPTOR, UDPBurstTask.TYPE); + measurementTypes.put(RRCTask.TYPE, RRCTask.class); + measurementTypes.put(PageLoadTimeTask.TYPE, PageLoadTimeTask.class); + measurementTypes.put(SequentialTask.TYPE, SequentialTask.class); +// measurementDescToType.put(PageLoadTimeTask.DESCRIPTOR, PageLoadTimeTask.TYPE); + measurementTypes.put(VideoQoETask.TYPE, VideoQoETask.class); + + // Hongyi: RRCTask is not accessible by users. So we don't put RRC descriptor + // and type into this map +// measurementDescToType.put(RRCTask.DESCRIPTOR, RRCTask.TYPE); + } + + /** + * @param measurementDesc + * @param parent + */ + protected MeasurementTask(MeasurementDesc measurementDesc) { + super(); + this.measurementDesc = measurementDesc; + generateTaskID(); + } + + /* Compare priority as the first order. Then compare start time. */ + @Override + public int compareTo(Object t) { + MeasurementTask another = (MeasurementTask) t; + + if (this.measurementDesc.startTime != null && + another.measurementDesc.startTime != null) { + return this.measurementDesc.startTime.compareTo( + another.measurementDesc.startTime); + } + return 0; + } + + public long timeFromExecution() { + return this.measurementDesc.startTime.getTime() - System.currentTimeMillis(); + } + + public boolean isPassedDeadline() { + if (this.measurementDesc.endTime == null) { + return false; + } else { + long endTime = this.measurementDesc.endTime.getTime(); + return endTime <= System.currentTimeMillis(); + } + } + + public String getMeasurementType() { + return this.measurementDesc.type; + } + + public String getKey() { + return this.measurementDesc.key; + } + + public void setKey(String key) { + this.measurementDesc.key = key; + } + + // added by Clarence, pass scheduler to every specific task + public void setContext(Context context) { + this.context = context; + } + + public Context getContext() { + return this.context; + } + + + public MeasurementDesc getDescription() { + return this.measurementDesc; + } + + /** Gets the currently available measurement descriptions */ + public static Set getMeasurementNames() { + return measurementDescToType.keySet(); + } + + /** Gets the currently available measurement types */ + public static Set getMeasurementTypes() { + return measurementTypes.keySet(); + } + + /** + * Get the type of a measurement based on its name. Type is for JSON interface only where as + * measurement name is a readable string for the UI + */ + public static String getTypeForMeasurementName(String name) { + return measurementDescToType.get(name); + } + + public static Class getTaskClassForMeasurement(String type) { + return measurementTypes.get(type); + } + + /* + * This is put here for consistency that all MeasurementTask should have a + * getDescClassForMeasurement() method. However, the MeasurementDesc is abstract and cannot be + * instantiated + */ + public static Class getDescClass() throws InvalidClassException { + throw new InvalidClassException("getDescClass() should only be invoked on " + + "subclasses of MeasurementTask."); + } + + public String getTaskId() { + return taskId; + } + + /** + * Returns a brief human-readable descriptor of the task. + */ + public abstract String getDescriptor(); + + @Override + public abstract MeasurementResult[] call() throws MeasurementError; + + /** Return the string indicating the measurement type. */ + public abstract String getType(); + + @Override + public abstract MeasurementTask clone(); + + /** + * Stop the measurement, even when it is running. There is no side effect if the measurement has + * not started or is already finished. + */ + public abstract boolean stop(); + + + public abstract long getDuration(); + + public abstract void setDuration(long newDuration); + + + @Override + public boolean equals(Object o) { + MeasurementTask another = (MeasurementTask) o; + if (this.getDescription().equals(another.getDescription()) + && this.getType().equals(another.getType())) { + return true; + } + return false; + } + + @Override + public int hashCode() { + StringBuilder taskstrbld = new StringBuilder(getMeasurementType()); + taskstrbld.append(",") + .append(this.measurementDesc.key).append(",") + .append(this.measurementDesc.startTime).append(",") + .append(this.measurementDesc.endTime).append(",") + .append(this.measurementDesc.intervalSec).append(",") + .append(this.measurementDesc.priority); + + Object[] keys = this.measurementDesc.parameters.keySet().toArray(); + Arrays.sort(keys); + for (Object k : keys) { + taskstrbld.append(",").append(this.measurementDesc.parameters.get((String) k)); + } + + return taskstrbld.toString().hashCode(); + } + + /** + * return hashcode of MeasurementTask as taskId. + */ + public void generateTaskID() { + taskId = this.hashCode() + ""; + } + + protected MeasurementTask(Parcel in) { +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + measurementDesc = in.readParcelable(MeasurementDesc.class.getClassLoader()); + taskId = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(measurementDesc, flags); + dest.writeString(taskId); + } + + /** + * All measurement tasks must provide measurements of how much data they have + * used to be fetched when the task completes. This allows us to make sure we + * stay under the data limit. + * + * @return Data consumed, in bytes + */ + public abstract long getDataConsumed(); + +} diff --git a/src/com/mobilyzer/PLTExecutorService.java b/src/com/mobilyzer/PLTExecutorService.java new file mode 100644 index 0000000..b9b1d8f --- /dev/null +++ b/src/com/mobilyzer/PLTExecutorService.java @@ -0,0 +1,79 @@ +package com.mobilyzer; + + + +import com.mobilyzer.util.AndroidWebView; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.AndroidWebView.WebViewProtocol; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class PLTExecutorService extends Service { + boolean spdyTest; + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + boolean spdy=intent.getBooleanExtra(UpdateIntent.PLT_TASK_PAYLOAD_TEST_TYPE,false); + spdyTest=spdy; + String url=intent.getStringExtra(UpdateIntent.PLT_TASK_PAYLOAD_URL); + long startTimeFilter=intent.getLongExtra(UpdateIntent.PLT_TASK_PAYLOAD_STARTTIME,0); + Logger.d("ashkan_plt: PLTExecutorService is started "+spdy+" "+url); + if(spdy){ + +// AndroidWebView spdyWebView=new AndroidWebView(this, false, startTimeFilter, url); + AndroidWebView webView=new AndroidWebView(this, true, WebViewProtocol.HTTP,startTimeFilter, url); + webView.loadUrl(); +// try { +// Thread.sleep(50000); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + +// spdyWebView.destroy(); +// AndroidWebView httpWebView=new AndroidWebView(this, false, startTimeFilter); +// httpWebView.loadUrl(url); + + }else{ + AndroidWebView webView=new AndroidWebView(this, false, WebViewProtocol.HTTP,startTimeFilter, url); + webView.loadUrl(); +// Intent newintent = new Intent(this, CrosswalkActivity.class); +// newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_URL, url); +// newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_STARTTIME, startTimeFilter); +// newintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK ); +// startActivity(newintent); + +// ChromiumWebView chWebview = new ChromiumWebView(this); +// chWebview.getAwContents().clearCache(true); +// chWebview.getAwContents().getSettings().setJavaScriptEnabled(true); +// chWebview.getAwContents().loadUrl(new LoadUrlParams(url)); + } + + return Service.START_NOT_STICKY; + } + @Override + public IBinder onBind(Intent intent) { + // TODO Auto-generated method stub + return null; + } + + + @Override + public void onDestroy() { + Logger.d("ashkan_plt: PLTExecutorService: onDestroy"); +// if(!spdyTest){ +// Intent closeIntent=new Intent(UpdateIntent.PLT_MEASUREMENT_ACTION); +// closeIntent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_CLOSE_ACTIVITY, "True"); +// sendBroadcast(closeIntent); +// +// } + // TODO Auto-generated method stub + super.onDestroy(); + } +} diff --git a/src/com/mobilyzer/PreemptibleMeasurementTask.java b/src/com/mobilyzer/PreemptibleMeasurementTask.java new file mode 100644 index 0000000..c7de3ba --- /dev/null +++ b/src/com/mobilyzer/PreemptibleMeasurementTask.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; +/** + * Interface for preemptable tasks. + * @author Ashkan Nikravesh (ashnik@umich.edu) + * + */ +public interface PreemptibleMeasurementTask { + public boolean pause(); + + //returns the task's total running time before it get paused + public long getTotalRunningTime(); + public void updateTotalRunningTime(long duration); + +} diff --git a/src/com/mobilyzer/ResourceCapManager.java b/src/com/mobilyzer/ResourceCapManager.java new file mode 100644 index 0000000..003c152 --- /dev/null +++ b/src/com/mobilyzer/ResourceCapManager.java @@ -0,0 +1,343 @@ +/* + * 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; + +import com.mobilyzer.MeasurementScheduler.DataUsageProfile; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; + +import android.content.Context; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; + +/** + * A basic power manager implementation that decides whether a measurement can be scheduled based on + * the current battery level: no measurements will be scheduled if the current battery is lower than + * a threshold. + */ +public class ResourceCapManager { + + + + /** The minimum threshold below which no measurements will be scheduled */ + private int minBatteryThreshold; + private Context context = null; + private int dataLimit;// in Byte + private DataUsageProfile dataUsageProfile; + + // Constants for how much data can be consumed under each profile + private static int UNLIMITED_LIMIT = -1; + private static int PROFILE1_LIMIT = 50 * 1024 * 1024; + private static int PROFILE2_LIMIT = 100 * 1024 * 1024; + private static int PROFILE3_LIMIT = 250 * 1024 * 1024; + private static int PROFILE4_LIMIT = 500 * 1024 * 1024; + + // Looking up various phone util data uses the network. + // It's hard to measure how much. + // The good news is that this value is basically constant! + public static int PHONEUTILCOST = 3 * 1024; + + public ResourceCapManager(int batteryThresh, Context context) { + this.minBatteryThreshold = batteryThresh; + this.dataLimit = PROFILE3_LIMIT; + this.context = context; + this.dataUsageProfile = DataUsageProfile.PROFILE3; + } + + /** + * Sets the minimum battery percentage below which measurements cannot be run. + * + * @param batteryThresh the battery percentage threshold between 0 and 100 + */ + public synchronized void setBatteryThresh(int batteryThresh) throws IllegalArgumentException { + // if (batteryThresh < 0 || batteryThresh > 100) { + // throw new IllegalArgumentException("batteryCap must fall between 0 and 100, inclusive"); + // } + this.minBatteryThreshold = batteryThresh; + } + + public synchronized int getBatteryThresh() { + return this.minBatteryThreshold; + } + + /** + * Given a data profile, set the data limit and profile code accordingly. + * + * If an invalid code is given, leave as default (250 MB) and print a warning. + * + * @param dataLimitStr String describing the profile + */ + public synchronized void setDataUsageLimit(DataUsageProfile profile) { + // if (dataLimitStr.equals("50 MB")) { + // dataLimit = PROFILE1_LIMIT; + // dataUsageProfile = DataUsageProfile.PROFILE1; + // } else if (dataLimitStr.equals("100 MB")) { + // dataLimit = PROFILE2_LIMIT; + // dataUsageProfile = DataUsageProfile.PROFILE2; + // } else if (dataLimitStr.equals("250 MB")) { + // dataLimit = PROFILE3_LIMIT; + // dataUsageProfile = DataUsageProfile.PROFILE3; + // } else if (dataLimitStr.equals("500 MB")) { + // dataLimit = PROFILE4_LIMIT; + // dataUsageProfile = DataUsageProfile.PROFILE4; + // } else if (dataLimitStr.equals("Unlimited")) { + // dataLimit = UNLIMITED_LIMIT; + // dataUsageProfile = DataUsageProfile.UNLIMITED; + // } else { + // Logger.w("Specified limit " + dataLimitStr + " not found!"); + // } + dataUsageProfile=profile; + if (profile.equals(DataUsageProfile.PROFILE1)) { + dataLimit = PROFILE1_LIMIT; + } else if (profile.equals(DataUsageProfile.PROFILE2)) { + dataLimit = PROFILE2_LIMIT; + } else if (profile.equals(DataUsageProfile.PROFILE3)) { + dataLimit = PROFILE3_LIMIT; + } else if (profile.equals(DataUsageProfile.PROFILE4)) { + dataLimit = PROFILE4_LIMIT; + } else if (profile.equals(DataUsageProfile.UNLIMITED)) { + dataLimit = UNLIMITED_LIMIT; + } else { + Logger.w("Specified limit profile not found!"); + } + + } + + /** + * @return The current data limit in bytes. + */ + public synchronized int getDataLimit() { + return this.dataLimit; + } + + /** + * @return An enum representing the data usage limit. + */ + public synchronized DataUsageProfile getDataUsageProfile() { + return this.dataUsageProfile; + } + + /** + * Reset the data used in the data usage file to 0. This should never be done unless the file does + * not exist. + */ + private void resetDataUsage() { + File file = new File(context.getFilesDir(), "datausage"); + if (file.exists()) { + Logger.e("Attempting to overwrite a file that exists!!!!"); + } + long usageStartTimeSec = (System.currentTimeMillis() / 1000); + writeDataUsageToFile(0, usageStartTimeSec); + } + + + /** + * Store the data used this period and the beginning of the period in a file, in the format [time + * reset, in seconds]_[bytes used]. + * + * Note that the data used can be negative, due to a underused data budget from last period. + * + * @param dataUsed The updated amount of data to write + * @param time The updated time to write + */ + private synchronized void writeDataUsageToFile(long dataUsed, long time) { + try { + FileOutputStream outputStream = context.openFileOutput("datausage", Context.MODE_PRIVATE); + String usageStat = time + "_" + dataUsed; + outputStream.write(usageStat.getBytes()); + Logger.i("Updating data usage: " + dataUsed + " Byte used from " + time); + outputStream.close(); + } catch (IOException e) { + Logger.e("Error in creating data usage file"); + e.printStackTrace(); + } + } + + /** + * Read the usage data (start of usage period and quantity used in bytes) from the usage data + * file. + * + * @return An array consisting of the start of the usage period, then the data used so far. If the + * file does not exist, returns -1 in each argument. + */ + private synchronized long[] readUsageFromFile() { + long[] retval = {-1, -1}; + File file = new File(context.getFilesDir(), "datausage"); + if (!file.exists()) { + return retval; + } + try { + String content = ""; + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + while ((line = br.readLine()) != null) { + content += line; + } + String[] toks = content.split("_"); + long usageStartTimeSec = Long.parseLong(toks[0]); + long dataUsed = Long.parseLong(toks[1]); + + retval[0] = usageStartTimeSec; + retval[1] = dataUsed; + + br.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return retval; + } + + /** + * Updates the data consumption period: using the current time move ahead to the correct data + * consumption period, and also update the data used so far. + * + * Data assigned to a previous data period is subtracted; this can go below zero, effectively + * crediting unused data to future tasks. + * + * @param dataUsed Data consumed since the start of the last period + * @param usageStartTimeSec Time since the start of the last period + * @return + */ + private long setNewDataConsumptionPeriod(long dataUsed, long usageStartTimeSec) { + long time_per_period = Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60; + + Logger.i("Finished data consumption period that began at time:" + usageStartTimeSec + + " having " + dataUsed + " consumed"); + + // Figure out how many periods have passed + int periods = + (int) (((float) ((long) (System.currentTimeMillis() / 1000) - usageStartTimeSec)) / (float) time_per_period); + + // Update usageStarTimeSec to the appropriate period + usageStartTimeSec += periods * time_per_period; + + // Discount from the data used data that is budgeted to previous periods. + // Note that this could go less than zero if we are below budget. + long datalimit_per_period = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30; + dataUsed = dataUsed - (((int) (periods)) * datalimit_per_period); + Logger.i("Net data usage at start of period: " + dataUsed); + + writeDataUsageToFile(dataUsed, usageStartTimeSec); + return dataUsed; + + } + + /** + * Helper function: given the beginning of the data usage period currrently under consideration, + * determine if we're still in that period. + * + * @param usageStartTimeSec The start of the last stored data usage period + * @return True if we are still in the same data usage period. + */ + private boolean isInDataLimitPeriod(long usageStartTimeSec) { + long timeSoFar = (System.currentTimeMillis() / 1000) - usageStartTimeSec; + Logger.i("Time passed since data period last changed: " + timeSoFar); + return timeSoFar <= Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60; + } + + /** + * Determines if the data limit has been exceeded. + * + * If there is no data limit, always returns false. If there is no valid data usage file, creates + * a new one and returns false. + * + * Otherwise, checks if we are over the limit yet or if we can run another task. If a new data + * period needs to be started, we do that too. * + * + * @param nextTaskType In the case of a TCP throughput task, we only run it if there is enough + * data left. + * @return True if over the data limit + * @throws IOException + */ + public boolean isOverDataLimit(String nextTaskType) throws IOException { + Logger.i("Checking data limit..."); + + if (getDataLimit() == UNLIMITED_LIMIT) { + Logger.i("No data limit!"); + return false; + } + long[] usagedata = readUsageFromFile(); + long usageStartTimeSec = usagedata[0]; + long dataUsed = usagedata[1]; + + if (usageStartTimeSec != -1) { + if (!isInDataLimitPeriod(usageStartTimeSec)) { + // Update our file to the next period, and update our data usage + // budget accordingly. + dataUsed = setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec); + } + long dataLimit = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30; + Logger.i("Data limit is: " + dataLimit + " Data used is:" + dataUsed); + if (dataUsed >= dataLimit) { + Logger.i("Exceeded data limit: Total data limit:" + getDataLimit()); + return true; + } else { + return false; + } + } + // If the file wasn't there we need to reset the data limit period. + resetDataUsage(); + return false; + } + + /** + * Determine how much data was consumed by a task and update the data usage accordingly. + * + * @param result Structure holding the measurement result from which we can extract data usage. + * @param taskType The type of measurement task completed + * @throws IOException + */ + public void updateDataUsage(long taskDataUsed) throws IOException { + + Logger.i("Amount of data used in the last task: " + taskDataUsed); + + long[] usagedata = readUsageFromFile(); + long usageStartTimeSec = usagedata[0]; + long dataUsed = usagedata[1]; + // If we have a valid file + if (usageStartTimeSec != -1) { + dataUsed += taskDataUsed; + if (!isInDataLimitPeriod(usageStartTimeSec)) { + // If we are in a new data consumption period, update it + setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec); + } else { + // Otherwise just write to a file + writeDataUsageToFile(dataUsed, usageStartTimeSec); + } + } else { + // If we don't have a data usage file, initialize it with the data just used + Logger.i("Data usage file not found, creating a new one..."); + usageStartTimeSec = (System.currentTimeMillis() / 1000); + dataUsed = taskDataUsed; + writeDataUsageToFile(dataUsed, usageStartTimeSec); + } + } + + + + /** + * Returns whether a measurement can be run. + */ + public synchronized boolean canScheduleExperiment() { + return (PhoneUtils.getPhoneUtils().isCharging() || PhoneUtils.getPhoneUtils() + .getCurrentBatteryLevel() > minBatteryThreshold); + } + +} diff --git a/src/com/mobilyzer/ServerMeasurementTask.java b/src/com/mobilyzer/ServerMeasurementTask.java new file mode 100644 index 0000000..4dcbd5c --- /dev/null +++ b/src/com/mobilyzer/ServerMeasurementTask.java @@ -0,0 +1,193 @@ +/* Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.Callable; + +import android.content.Intent; + +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.exceptions.MeasurementSkippedException; +import com.mobilyzer.measurements.ParallelTask; +import com.mobilyzer.measurements.SequentialTask; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; + +public class ServerMeasurementTask implements Callable { + private MeasurementTask realTask; + private MeasurementScheduler scheduler; + private ContextCollector contextCollector; + private ResourceCapManager rManager; + + public ServerMeasurementTask(MeasurementTask task, + MeasurementScheduler scheduler, ResourceCapManager manager) { + realTask = task; + realTask.setContext(null); //added by Clarence + this.scheduler = scheduler; + this.contextCollector = new ContextCollector(); + this.rManager = manager; + } + + /** + * Notify the scheduler that this task is started + */ + private void broadcastMeasurementStart() { + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_STARTED); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, realTask.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, realTask.getKey()); + scheduler.sendBroadcast(intent); + } + + /** + * Notify the scheduler that this task is finished executing. The result can + * be completed, paused or failed due to exception + * + * @param results + * Results of the task + * @param error + * Measurement error leading to task's failure + */ + private void broadcastMeasurementEnd(MeasurementResult[] results, + MeasurementError error) { + + // Only broadcast information about measurements if they are true + // errors. + if (!(error instanceof MeasurementSkippedException)) { + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, + Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + (int) realTask.getDescription().priority); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, realTask.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, realTask.getKey()); + intent.putExtra(UpdateIntent.TASK_TYPE_PAYLOAD, realTask.getType()); + +// if (realTask.getType().equals(SequentialTask.TYPE) || realTask.getType().equals(ParallelTask.TYPE)){ +// intent.putExtra(UpdateIntent.TASK_DESC_PAYLOAD, realTask.getDescription()); +// } + + if (results != null) { + // Only single task can be paused + if (results[0].getTaskProgress() == TaskProgress.PAUSED) { + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, + Config.TASK_PAUSED); + } else if (results[0].getTaskProgress() == TaskProgress.RESCHEDULED) { + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, + Config.TASK_RESCHEDULED); + intent.putExtra(UpdateIntent.RESULT_PAYLOAD, results); + } else { + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, + Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.RESULT_PAYLOAD, results); + } + scheduler.sendBroadcast(intent); + } + } + + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + MeasurementResult[] results = null; + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + try { + phoneUtils.acquireWakeLock(); + + // if(!(phoneUtils.isCharging() || + // phoneUtils.getCurrentBatteryLevel() > + // rManager.getBatteryThresh())){ + // throw new + // MeasurementSkippedException("Not enough battery power"); + // } + + if (!rManager.canScheduleExperiment()) { + throw new MeasurementSkippedException( + "Not enough battery power"); + } + + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + try { + if (rManager.isOverDataLimit(realTask.getMeasurementType())) { + Logger.i("Skipping measurement - data limit is passed"); + throw new MeasurementSkippedException("Over data limit"); + } + } catch (IOException e) { + Logger.e("Exception occured during R/Wing of data stat file"); + e.printStackTrace(); + } + + } + + broadcastMeasurementStart(); + try { + contextCollector + .setInterval(realTask.getDescription().contextIntervalSec); + contextCollector.startCollector(); + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + rManager.updateDataUsage(ResourceCapManager.PHONEUTILCOST); + } + results = realTask.call(); + ArrayList> contextResults = contextCollector + .stopCollector(); + for (MeasurementResult r : results) { + r.addContextResults(contextResults); + r.getDeviceProperty().dnResolvability = contextCollector.dnsConnectivity; + r.getDeviceProperty().ipConnectivity = contextCollector.ipConnectivity; + } + + + if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) { + rManager.updateDataUsage(realTask.getDataConsumed()); + } + +// if (realTask.getDescription().priority == MeasurementTask.GCM_PRIORITY) { +// this.scheduler.checkin.uploadGCMMeasurementResult( +// results[0], rManager); +// } + broadcastMeasurementEnd(results, null); + } catch (MeasurementError e) { + String error = "Server measurement " + realTask.getDescriptor() + + " has failed: " + e.getMessage() + "\n"; + Logger.e(error); + results = MeasurementResult.getFailureResult(realTask, e); + broadcastMeasurementEnd(results, e); + + } catch (Exception e) { + String error = "Server measurement " + realTask.getDescriptor() + + " has failed\n"; + error += "Unexpected Exception: " + e.getMessage() + "\n"; + Logger.e(error); + + results = MeasurementResult.getFailureResult(realTask, e); + broadcastMeasurementEnd(results, new MeasurementError( + "Got exception running task", e)); + } + } finally { + phoneUtils.releaseWakeLock(); + MeasurementTask currentTask = scheduler.getCurrentTask(); + if (currentTask != null && currentTask.equals(realTask)) { + scheduler.setCurrentTask(null); + } + } + return results; + } +} diff --git a/src/com/mobilyzer/UpdateIntent.java b/src/com/mobilyzer/UpdateIntent.java new file mode 100644 index 0000000..f6be044 --- /dev/null +++ b/src/com/mobilyzer/UpdateIntent.java @@ -0,0 +1,103 @@ +package com.mobilyzer; +import java.security.InvalidParameterException; + +import android.content.Intent; + +import android.os.Process; +/** + * A repackaged Intent class that includes MobiLib-specific information. + */ +public class UpdateIntent extends Intent { + + // Different types of payloads that this intent can carry: + + public static final String TASKID_PAYLOAD = "TASKID_PAYLOAD"; + public static final String CLIENTKEY_PAYLOAD = "CLIENTKEY_PAYLOAD"; + public static final String TASK_PRIORITY_PAYLOAD = "TASK_PRIORITY_PAYLOAD"; + public static final String TASK_TYPE_PAYLOAD = "TASK_TYPE_PAYLOAD"; + public static final String TASK_DESC_PAYLOAD = "TASK_DESC_PAYLOAD"; + public static final String RESULT_PAYLOAD = "RESULT_PAYLOAD"; + public static final String MEASUREMENT_TASK_PAYLOAD = "MEASUREMENT_TASK_PAYLOAD"; + public static final String BATTERY_THRESHOLD_PAYLOAD = "BATTERY_THRESHOLD_PAYLOAD"; + public static final String CHECKIN_INTERVAL_PAYLOAD = "CHECKIN_INTERVAL_PAYLOAD"; + public static final String TASK_STATUS_PAYLOAD = "TASK_STATUS_PAYLOAD"; + public static final String DATA_USAGE_PAYLOAD = "DATA_USAGE_PAYLOAD"; + public static final String VERSION_PAYLOAD = "VERSION_PAYLOAD"; + public static final String AUTH_ACCOUNT_PAYLOAD = "AUTH_ACCOUNT_PAYLOAD"; + public static final String PLT_TASK_PAYLOAD_URL = "PLT_TASK_PAYLOAD_URL"; + public static final String PLT_TASK_PAYLOAD_STARTTIME = "PLT_TASK_PAYLOAD_STARTTIME"; + public static final String PLT_TASK_PAYLOAD_TEST_TYPE = "PLT_TASK_PAYLOAD_TEST_TYPE"; + public static final String PLT_TASK_PAYLOAD_RESULT_RES = "PLT_TASK_PAYLOAD_RESULT_RES"; + public static final String PLT_TASK_PAYLOAD_RESULT_NAV = "PLT_TASK_PAYLOAD_RESULT_NAV"; + public static final String PLT_TASK_PAYLOAD_BYTE_USED = "PLT_TASK_PAYLOAD_BYTE_USED"; +// public static final String PLT_TASK_PAYLOAD_RESULT_NUM = "PLT_TASK_PAYLOAD_RESULT_NUM"; +// public static final String PLT_TASK_PAYLOAD_CLOSE_ACTIVITY = "PLT_TASK_PAYLOAD_CLOSE_ACTIVITY"; +// public static final String PLT_TASK_PAYLOAD_START_ACTIVITY = "PLT_TASK_PAYLOAD_START_ACTIVITY"; + public static final String VIDEO_TASK_PAYLOAD_IS_SUCCEED = "VIDEO_TASK_PAYLOAD_IS_SUCCEED"; + public static final String VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED = "VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED"; + public static final String VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP = "VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP"; + public static final String VIDEO_TASK_PAYLOAD_GOODPUT_VALUE = "VIDEO_TASK_PAYLOAD_GOODPUT_VALUE"; + public static final String VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE = "VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE"; + public static final String VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP = "VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP"; + public static final String VIDEO_TASK_PAYLOAD_BITRATE_VALUE = "VIDEO_TASK_PAYLOAD_BITRATE_VALUE"; + public static final String VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME = "VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME"; + public static final String VIDEO_TASK_PAYLOAD_REBUFFER_TIME = "VIDEO_TASK_PAYLOAD_REBUFFER_TIME"; + public static final String VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME = "VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME"; + public static final String VIDEO_TASK_PAYLOAD_BYTE_USED = "VIDEO_TASK_PAYLOAD_BYTE_USED"; + //added by Clarence + public static final String INTERMEDIATE_RESULT_PAYLOAD = "INTERMEDIATE_RESULT_PAYLOAD"; + + + // Different types of actions that this intent can represent: + private static final String PACKAGE_PREFIX = + UpdateIntent.class.getPackage().getName(); + private static final String APP_PREFIX = + UpdateIntent.class.getPackage().getName() + Process.myPid(); + + public static final String MEASUREMENT_ACTION = + APP_PREFIX + ".MEASUREMENT_ACTION"; + public static final String CHECKIN_ACTION = + APP_PREFIX + ".CHECKIN_ACTION"; + public static final String CHECKIN_RETRY_ACTION = + APP_PREFIX + ".CHECKIN_RETRY_ACTION"; + public static final String MEASUREMENT_PROGRESS_UPDATE_ACTION = + APP_PREFIX + ".MEASUREMENT_PROGRESS_UPDATE_ACTION"; + public static final String GCM_MEASUREMENT_ACTION = + APP_PREFIX + ".GCM_MEASUREMENT_ACTION"; + public static final String PLT_MEASUREMENT_ACTION = + APP_PREFIX + ".PLT_MEASUREMENT_ACTION"; + public static final String VIDEO_MEASUREMENT_ACTION = + APP_PREFIX + ".VIDEO_MEASUREMENT_ACTION"; + + public static final String USER_RESULT_ACTION = + PACKAGE_PREFIX + ".USER_RESULT_ACTION"; + public static final String SERVER_RESULT_ACTION = + PACKAGE_PREFIX + ".SERVER_RESULT_ACTION"; + public static final String BATTERY_THRESHOLD_ACTION = + PACKAGE_PREFIX + ".BATTERY_THRESHOLD_ACTION"; + public static final String CHECKIN_INTERVAL_ACTION = + PACKAGE_PREFIX + ".CHECKIN_INTERVAL_ACTION"; + public static final String TASK_STATUS_ACTION = + PACKAGE_PREFIX + ".TASK_STATUS_ACTION"; + public static final String DATA_USAGE_ACTION = + PACKAGE_PREFIX + ".DATA_USAGE_ACTION"; + public static final String AUTH_ACCOUNT_ACTION = + PACKAGE_PREFIX + ".AUTH_ACCOUNT_ACTION"; + + + // added by Clarence + public static final String MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION = + APP_PREFIX + ".MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION"; + + /** + * Creates an intent of the specified action with an optional message + */ + protected UpdateIntent(String action) + throws InvalidParameterException { + super(); + if (action == null) { + throw new InvalidParameterException("action of UpdateIntent should not be null"); + } + this.setAction(action); + } +} diff --git a/src/com/mobilyzer/UserMeasurementTask.java b/src/com/mobilyzer/UserMeasurementTask.java new file mode 100644 index 0000000..7940838 --- /dev/null +++ b/src/com/mobilyzer/UserMeasurementTask.java @@ -0,0 +1,131 @@ +/* Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.Callable; + +import android.content.Context; +import android.content.Intent; + +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.PhoneUtils; + +public class UserMeasurementTask implements Callable { + private MeasurementTask realTask; + private MeasurementScheduler scheduler; + private ContextCollector contextCollector; + + public UserMeasurementTask(MeasurementTask task, + MeasurementScheduler scheduler) { + realTask = task; + realTask.setContext(scheduler.getApplicationContext()); //added by Clarence + this.scheduler = scheduler; + this.contextCollector= new ContextCollector(); + } + + /** + * Notify the scheduler that this task is started + */ + private void broadcastMeasurementStart() { + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_STARTED); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, realTask.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, realTask.getKey()); +// Context context = scheduler.getApplicationContext(); +// context.sendBroadcast(intent); + scheduler.sendBroadcast(intent); + + } + + /** + * Notify the scheduler that this task is finished executing. + * The result can be completed, paused or failed due to exception + * @param results Results of the task + */ + private void broadcastMeasurementEnd(MeasurementResult[] results) { + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, realTask.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, realTask.getKey()); + + if (results != null){ + //TODO only single task can be paused + if(results[0].getTaskProgress()==TaskProgress.PAUSED){ + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_PAUSED); + } + else{ + intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.RESULT_PAYLOAD, results); + } + scheduler.sendBroadcast(intent); + } + + } + + /** + * The call() method that broadcast intents before the measurement starts + * and after the measurement finishes. + */ + @Override + public MeasurementResult[] call() throws MeasurementError { + MeasurementResult[] results = null; + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + try { + phoneUtils.acquireWakeLock(); + broadcastMeasurementStart(); + contextCollector.setInterval(realTask.getDescription().contextIntervalSec); + contextCollector.startCollector(); + results = realTask.call(); + ArrayList> contextResults = + contextCollector.stopCollector(); + for (MeasurementResult r: results){ + String testIntermediate1 = r.toString(); + r.addContextResults(contextResults); + r.getDeviceProperty().dnResolvability=contextCollector.dnsConnectivity; + r.getDeviceProperty().ipConnectivity=contextCollector.ipConnectivity; + String testIntermediate2 = r.toString(); + + } + } catch (MeasurementError e) { + Logger.e("User measurement " + realTask.getDescriptor() + " has failed"); + Logger.e(e.getMessage()); + results = MeasurementResult.getFailureResult(realTask, e); + } catch (Exception e) { + Logger.e("User measurement " + realTask.getDescriptor() + " has failed"); + Logger.e("Unexpected Exception: " + e.getMessage()); + results = MeasurementResult.getFailureResult(realTask, e); + } finally { + broadcastMeasurementEnd(results); + MeasurementTask currentTask = scheduler.getCurrentTask(); + if(currentTask != null && currentTask.equals(realTask)){ + scheduler.setCurrentTask(null); + } + phoneUtils.releaseWakeLock(); + } + return results; + } + + +} diff --git a/src/com/mobilyzer/api/API.java b/src/com/mobilyzer/api/API.java new file mode 100644 index 0000000..5582737 --- /dev/null +++ b/src/com/mobilyzer/api/API.java @@ -0,0 +1,598 @@ +/* Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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.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.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; +import com.mobilyzer.MeasurementScheduler.DataUsageProfile; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.measurements.DnsLookupTask; +import com.mobilyzer.measurements.HttpTask; +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.PingDesc; +import com.mobilyzer.measurements.SequentialTask.SequentialDesc; +import com.mobilyzer.measurements.TCPThroughputTask.TCPThroughputDesc; +import com.mobilyzer.measurements.TracerouteTask.TracerouteDesc; +import com.mobilyzer.measurements.UDPBurstTask.UDPBurstDesc; +import com.mobilyzer.util.Logger; + +/** + * @author jackjia,Hongyi Yao (hyyao@umich.edu) + * The user API for Mobiperf library. + * Use singleton design pattern to ensure there only exist one instance of API + * User: create and add task => Scheduler: run task, send finish intent => + * 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 + } + + /** + * Action name of different type of result for broadcast receiver. + * userResultAction is not a constant value. We append the clientKey to + * UpdateIntent.USER_RESULT_ACTION so that only the user who submit the task + * can get the result + */ + public String userResultAction; + public static final String SERVER_RESULT_ACTION = + UpdateIntent.SERVER_RESULT_ACTION; + public String batteryThresholdAction; + public String checkinIntervalAction; + public String taskStatusAction; + public String dataUsageAction; + public String authAccountAction; + + 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; + + /** + * Singleton api object for the entire application + */ + private static API apiObject; + private Queue pendingMsg; + /** + * Make constructor private for singleton design + * @param parent Context when the object is created + * @param clientKey User-defined unique key for this application + */ + private API(Context parent, String clientKey) { + Logger.d("API: constructor is called..."); + this.applicationContext = parent.getApplicationContext(); + this.clientKey = clientKey; + this.pendingMsg = new LinkedList(); + + this.userResultAction = UpdateIntent.USER_RESULT_ACTION + "." + clientKey; + + this.batteryThresholdAction = UpdateIntent.BATTERY_THRESHOLD_ACTION + "." + + clientKey; + this.checkinIntervalAction = UpdateIntent.CHECKIN_INTERVAL_ACTION + "." + + clientKey; + this.taskStatusAction = UpdateIntent.TASK_STATUS_ACTION + "." + clientKey; + this.dataUsageAction = UpdateIntent.DATA_USAGE_ACTION + "." + clientKey; + this.authAccountAction = UpdateIntent.AUTH_ACCOUNT_ACTION + "." + clientKey; + startAndBindService(); + } + + /** + * Actual method to get the singleton API object + * @param parent Context which the object lies in + * @param clientKey User-defined unique key for this application + * @return Singleton API object + */ + public static API getAPI(Context parent, String clientKey) { + Logger.d("API: Get API Singeton object..."); + if ( apiObject == null ) { + Logger.d("API: API object not initialized..."); + apiObject = new API(parent, clientKey); + } + else { + // Safeguard to avoid using unbound API object + apiObject.startAndBindService(); + } + return apiObject; + } + + @Override + public Object clone() throws CloneNotSupportedException { + // Prevent the singleton object to be copied + throw new CloneNotSupportedException(); + } + + + /** Defines callbacks for binding and unbinding scheduler*/ + private ServiceConnection serviceConn = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + Logger.d("API -> onServiceConnected called"); + // We've bound to the scheduler's messenger and get Messenger instance + mSchedulerMessenger = new Messenger(service); + isBound = true; + isBindingToService = false; + + Logger.i("Register client key"); + Message msg = Message.obtain(null, Config.MSG_REGISTER_CLIENTKEY); + Bundle data = new Bundle(); + data.putString(UpdateIntent.CLIENTKEY_PAYLOAD, clientKey); + msg.setData(data); + try { + sendMessage(msg); + } catch (MeasurementError e) { + Logger.e("Register clientKey failed", e); + } + + Logger.i("Send pending message"); + while ( (msg = pendingMsg.poll()) != null ) { + try { + sendMessage(msg); + } catch (MeasurementError e) { + Logger.e("Send pending message failed", e); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + // This callback is never called until the active scheduler is uninstalled + Logger.d("API -> onServiceDisconnected called"); + mSchedulerMessenger = null; + isBound = false; + // Start and bind to another scheduler (probably bind to the one in itself) + Logger.e("API -> startAndBind again"); + startAndBindService(); + } + }; + + /** + * Bind to scheduler, automatically called when the API is initialized + */ + public void startAndBindService() { + Logger.e("API-> startAndBindService() called "+isBindingToService+" "+isBound); + if (!isBindingToService && !isBound) { + Logger.e("API-> bind() called 2"); + // Bind to the scheduler service if it is not bounded +// Intent intent = new Intent("com.mobilyzer.MeasurementScheduler"); + Intent intent = new Intent(applicationContext, MeasurementScheduler.class); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, clientKey); + intent.putExtra(UpdateIntent.VERSION_PAYLOAD, Config.version); + /** + * Start and bind to service if it is not bounded. + * Notice that we don't use BIND_AUTO_CREATE flag since it will prevent + * the scheduler to kill itself when stopSelf is called + */ + applicationContext.startService(intent); + applicationContext.bindService(intent, serviceConn, 0); + isBindingToService = true; + } + } + + /** + * Unbind from scheduler, called in activity's onDestroy callback function + */ + public void unbind() { + Logger.e("API-> unbind called"); + if (isBound) { + Logger.e("API-> unbind called 2"); + // Register client key + Message msg = Message.obtain(null, Config.MSG_UNREGISTER_CLIENTKEY); + Bundle data = new Bundle(); + data.putString(UpdateIntent.CLIENTKEY_PAYLOAD, clientKey); + msg.setData(data); + try { + sendMessage(msg); + } catch (MeasurementError e) { + Logger.e("Unregister clientKey failed", e); + } + // Unbind service + applicationContext.unbindService(serviceConn); + isBound = false; + } + } + + /** + * Create a new MeasurementTask based on those parameters. Then submit it to + * scheduler by addTask or put into task list of parallel or sequential task + * @param taskType Type of measurement (ping, dns, traceroute, etc.) for this + * measurement task. + * @param startTime Earliest time that measurements can be taken using this + * Task descriptor. The current time will be used in place of a null + * startTime parameter. Measurements with a startTime more than 24 + * hours from now will NOT be run. + * @param endTime Latest time that measurements can be taken using this Task + * descriptor. Tasks with an endTime before startTime will be canceled. + * Corresponding to the 24-hour rule in startTime, tasks with endTime + * later than 24 hours from now will be assigned a new endTime that + * ends 24 hours from now. + * @param intervalSec Minimum number of seconds to elapse between consecutive + * measurements taken with this description. + * @param count Maximum number of times that a measurement should be taken + * with this description. A count of 0 means to continue the + * measurement indefinitely (until end_time). + * @param priority Two level of priority: USER_PRIORITY for user task and + * INVALID_PRIORITY for server task + * @param contextIntervalSec interval between the context collection (in sec) + * @param params Measurement parameters. + * @return Measurement task filled with those parameters + * @throws MeasurementError taskType is not valid + */ + public MeasurementTask createTask( TaskType taskType, Date startTime + , Date endTime, double intervalSec, long count, long priority + , int contextIntervalSec, Map params) + throws MeasurementError { + MeasurementTask task = null; + switch ( taskType ) { + case DNSLOOKUP: + task = new DnsLookupTask(new DnsLookupDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; + case HTTP: + task = new HttpTask(new HttpDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; + case PING: + task = new PingTask(new PingDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; + case TRACEROUTE: + task = new TracerouteTask(new TracerouteDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; + case TCPTHROUGHPUT: + task = new TCPThroughputTask(new TCPThroughputDesc(clientKey, startTime + , endTime, intervalSec, count, priority, contextIntervalSec, params)); + break; + case UDPBURST: + task = new UDPBurstTask(new UDPBurstDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params)); + break; +// case PLT: +// task = new PageLoadTimeTask(new PageLoadTimeDesc(clientKey, startTime, endTime +// , intervalSec, count, priority, contextIntervalSec, params)); +// break; + default: + throw new MeasurementError("Undefined measurement type. Candidate: " + + "DNSLOOKUP, HTTP, PING, TRACEROUTE, TCPTHROUGHPUT, UDPBURST"); + } + return task; + } + + /** + * Create a parallel or sequential task based on the manner. An ArrayList of + * MeasurementTask must be provided as the real tasks to be executed + * @param manner Determine whether tasks in task list will be executed + * parallelly or sequentially (back-to-back) + * @param startTime Earliest time that measurements can be taken using this + * Task descriptor. The current time will be used in place of a null + * startTime parameter. Measurements with a startTime more than 24 + * hours from now will NOT be run. + * @param endTime Latest time that measurements can be taken using this Task + * descriptor. Tasks with an endTime before startTime will be canceled. + * Corresponding to the 24-hour rule in startTime, tasks with endTime + * later than 24 hours from now will be assigned a new endTime that + * ends 24 hours from now. + * @param intervalSec Minimum number of seconds to elapse between consecutive + * measurements taken with this description. + * @param count Maximum number of times that a measurement should be taken + * with this description. A count of 0 means to continue the + * measurement indefinitely (until end_time). + * @param priority Two level of priority: USER_PRIORITY for user task and + * INVALID_PRIORITY for server task + * @param contextIntervalSec interval between the context collection (in sec) + * @param params Measurement parameters. + * @param taskList tasks to be executed + * @return The parallel or sequential task filled with those parameters + * @throws MeasurementError manner is not valid + */ + public MeasurementTask composeTasks(TaskType manner, Date startTime, + Date endTime, double intervalSec, long count, long priority, + int contextIntervalSec, Map params, + ArrayList taskList) throws MeasurementError { + MeasurementTask task = null; + switch ( manner ) { + case PARALLEL: + task = new ParallelTask(new ParallelDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params), taskList); + break; + case SEQUENTIAL: + task = new SequentialTask(new SequentialDesc(clientKey, startTime, endTime + , intervalSec, count, priority, contextIntervalSec, params), taskList); + break; + default: + throw new MeasurementError("Undefined measurement composing type. " + + " Candidate: PARALLEL, SEQUENTIAL"); + } + return task; + } + + /** + * Get available messenger after binding to scheduler + * @return the messenger if bound, null otherwise + */ + private Messenger getScheduler() { + if (isBound) { + Logger.e("API -> get available messenger"); + return mSchedulerMessenger; + } else { + Logger.e("API -> have not bound to a scheduler!"); + return null; + } + } + + /** + * Helper method for sending messages to the scheduler + * @param msg message to be sent + * @throws MeasurementError + */ + private void sendMessage(Message msg) throws MeasurementError { + Messenger messenger = getScheduler(); + if ( messenger != null ) { + // Append client key to every msg sent from API + Bundle data = msg.getData(); + if (data == null) { + data = new Bundle(); + msg.setData(data); + } + data.putString(UpdateIntent.CLIENTKEY_PAYLOAD, clientKey); + + try { + messenger.send(msg); + } catch (RemoteException e) { + String err = "remote scheduler failed!"; + Logger.e(err); + throw new MeasurementError(err); + } + } + else { + String err = "API didn't bind to a scheduler. Message will be temporarily" + + " queued and sent after scheduler bound"; + Logger.e(err); + this.pendingMsg.offer(msg); + } + } + + /** + * Submit task to the scheduler. + * Works in async way. The result will be returned in a intent whose action is + * USER_RESULT_ACTION + clientKey or SERVER_RESULT_ACTION + * @param task the task to be exectued, created by createTask(..) + * or composeTask(..) + * @throws MeasurementError + */ + public void submitTask ( MeasurementTask task ) + throws MeasurementError { + Logger.d("API->submitTask called"); + if ( task != null ) { +// // Hongyi: for delay measurement +// task.getDescription().parameters.put("ts_api_send", +// String.valueOf(System.currentTimeMillis())); + + Logger.i("API: Adding new " + task.getType() + " task " + task.getTaskId()); + Message msg = Message.obtain(null, Config.MSG_SUBMIT_TASK); + Bundle data = new Bundle(); + data.putParcelable(UpdateIntent.MEASUREMENT_TASK_PAYLOAD, task); + msg.setData(data); + sendMessage(msg); + } + else { + String err = "submitTask: task is null"; + Logger.e(err); + throw new MeasurementError(err); + } + } + + /** + * Cancel the task submitted to the scheduler + * @param localId task to be cancelled. Got by MeasurementTask.getTaskId() + * @return true for succeed, false for fail + * @throws InvalidParameterException + */ + public void cancelTask(String taskId) throws MeasurementError{ + Logger.d("API->cancelTask called"); + if ( taskId != null ) { + Message msg = Message.obtain(null, Config.MSG_CANCEL_TASK); + Bundle data = new Bundle(); + Logger.i("API: try to cancel task " + taskId); + data.putString(UpdateIntent.TASKID_PAYLOAD, taskId); + msg.setData(data); + sendMessage(msg); + } + else { + String err = "cancelTask: taskId is null"; + Logger.e(err); + throw new MeasurementError(err); + } + } + + /** + * Set authenticate account for uploading results. Anonymous by default + * @param account + * @throws MeasurementError + */ + public void setAuthenticateAccount(String account) throws MeasurementError { + Logger.d("API->setAuthenticateAccount called"); + Message msg = Message.obtain(null, Config.MSG_SET_AUTH_ACCOUNT); + Bundle data = new Bundle(); + data.putString(UpdateIntent.AUTH_ACCOUNT_PAYLOAD, account); + msg.setData(data); + sendMessage(msg); + } + + /** + * Get current authenticate account used by scheduler + * @throws MeasurementError + */ + public void getAuthenticateAccount() throws MeasurementError { + Logger.d("API->getAuthenticateAccount called"); + Message msg = Message.obtain(null, Config.MSG_GET_AUTH_ACCOUNT); + sendMessage(msg); + } + /** + * Set battery threshold of the scheduler. Only a threshold larger than the + * current one will be accepted. + * @param threshold new battery threshold, must stay between 0 and 100 + * @throws MeasurementError + */ + public void setBatteryThreshold(int threshold) throws MeasurementError { + Logger.d("API->setBatteryThreshold called"); + if ( threshold > 100 || threshold <= 0 ) { + String err = "Battery threshold should stay between 0 and 100"; + Logger.e(err); + throw new MeasurementError(err); + } + Message msg = Message.obtain(null, Config.MSG_SET_BATTERY_THRESHOLD); + Bundle data = new Bundle(); + data.putInt(UpdateIntent.BATTERY_THRESHOLD_PAYLOAD, threshold); + msg.setData(data); + sendMessage(msg); + } + + /** + * Get current battery threshold of the scheduler. + * Async call. Receive api.batteryThresholdAction to get the result + * @throws MeasurementError + */ + public void getBatteryThreshold() throws MeasurementError { + Logger.d("API->getBatteryThreshold called"); + Message msg = Message.obtain(null, Config.MSG_GET_BATTERY_THRESHOLD); + sendMessage(msg); + } + + /** + * Set checkin interval of the scheduler. Only an interval larger than the + * current one will be accepted. + * @param interval new checkin interval, should be greater than min interval + * @throws MeasurementError + */ + public void setCheckinInterval(long interval) throws MeasurementError { + Logger.d("API->setCheckinInterval called"); + if ( interval < Config.MIN_CHECKIN_INTERVAL_SEC ) { + String err = "Checkin interval should be greater than " + + Config.MIN_CHECKIN_INTERVAL_SEC; + Logger.e(err); + throw new MeasurementError(err); + } + Message msg = Message.obtain(null, Config.MSG_SET_CHECKIN_INTERVAL); + Bundle data = new Bundle(); + data.putLong(UpdateIntent.CHECKIN_INTERVAL_PAYLOAD, interval); + msg.setData(data); + sendMessage(msg); + } + + /** + * Get current checkin interval of the scheduler. + * Async call. Receive api.checkinIntervalAction to get the result + * @throws MeasurementError + */ + public void getCheckinInterval() throws MeasurementError { + Logger.d("API->getCheckinInterval called"); + Message msg = Message.obtain(null, Config.MSG_GET_CHECKIN_INTERVAL); + sendMessage(msg); + } + + /** + * Get current status of that task. + * Async call. Receive api.taskStatusAction to get the result + * @param taskId the id of the target task + * @throws MeasurementError + */ + public void getTaskStatus(String taskId) throws MeasurementError { + Message msg = Message.obtain(null, Config.MSG_GET_TASK_STATUS); + Bundle data = new Bundle(); + data.putString(UpdateIntent.TASKID_PAYLOAD, taskId); + msg.setData(data); + Logger.d("Attempt getting task status"); + sendMessage(msg); + } + + /** + * Set data usage profile of the scheduler. Only an profile more conventional + * than the current one will be accepted. + * @param profile new data usage profile + * @throws MeasurementError + */ + public void setDataUsage(DataUsageProfile profile) throws MeasurementError { + if ( profile == DataUsageProfile.NOTASSIGNED ) { + String err = "Data usage profile should be valid"; + Logger.e(err); + throw new MeasurementError(err); + } + Message msg = Message.obtain(null, Config.MSG_SET_DATA_USAGE); + Bundle data = new Bundle(); + data.putSerializable(UpdateIntent.DATA_USAGE_PAYLOAD, profile); + msg.setData(data); + Logger.d("Attempt setting data usage to " + profile); + sendMessage(msg); + + } + + /** + * Get current data usage of the scheduler. + * Async call. Receive api.dataUsageAction to get the result + * @throws MeasurementError + */ + public void getDataUsage() throws MeasurementError { + Message msg = Message.obtain(null, Config.MSG_GET_DATA_USAGE); + Logger.d("Attempt getting data usage"); + sendMessage(msg); + } + + /** Gets the currently available measurement descriptions*/ + public static Set getMeasurementNames() { + return MeasurementTask.getMeasurementNames(); + } + + /** Get the type of a measurement based on its name. Type is for JSON interface only + * where as measurement name is a readable string for the UI */ + public static String getTypeForMeasurementName(String name) { + return MeasurementTask.getTypeForMeasurementName(name); + } +} diff --git a/src/com/mobilyzer/exceptions/MeasurementError.java b/src/com/mobilyzer/exceptions/MeasurementError.java new file mode 100755 index 0000000..041ec7d --- /dev/null +++ b/src/com/mobilyzer/exceptions/MeasurementError.java @@ -0,0 +1,27 @@ +/* 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.exceptions; + +/** + * Error raised when a measurement fails. + */ +public class MeasurementError extends Exception { + public MeasurementError(String reason) { + super(reason); + } + public MeasurementError(String reason, Throwable e) { + super(reason, e); + } +} diff --git a/src/com/mobilyzer/exceptions/MeasurementSkippedException.java b/src/com/mobilyzer/exceptions/MeasurementSkippedException.java new file mode 100755 index 0000000..c240503 --- /dev/null +++ b/src/com/mobilyzer/exceptions/MeasurementSkippedException.java @@ -0,0 +1,31 @@ +/* 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.exceptions; + +/** + * Subclass of MeasurementError that indicates that + * a measurement was skipped - a non-error result. + */ +public class MeasurementSkippedException extends MeasurementError { + public MeasurementSkippedException(String reason) { + super(reason); + } + + public MeasurementSkippedException(String reason, Throwable e) { + super(reason, e); + } + +} diff --git a/src/com/mobilyzer/gcm/GCMManager.java b/src/com/mobilyzer/gcm/GCMManager.java new file mode 100644 index 0000000..66786bb --- /dev/null +++ b/src/com/mobilyzer/gcm/GCMManager.java @@ -0,0 +1,169 @@ +package com.mobilyzer.gcm; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.mobilyzer.util.Logger; + +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; + +public class GCMManager{ + + public static final String EXTRA_MESSAGE = "message"; + public static final String PROPERTY_REG_ID = "registration_id"; + private static final String PROPERTY_APP_VERSION = "appVersion"; + private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000; + + + + /** + * Substitute you own sender ID here. This is the project number you got + * from the API Console" + */ + String SENDER_ID = "120699632611"; + + + GoogleCloudMessaging gcm; + AtomicInteger msgId = new AtomicInteger(); + Context context; + + String regid; + + public GCMManager(Context context) { + this.context=context; + // Check device for Play Services APK. If check succeeds, proceed with GCM registration. + if (checkPlayServices()) { + gcm = GoogleCloudMessaging.getInstance(context); + regid = getRegistrationId(); + Logger.d("GCMManager: regid = "+regid); + + if (regid.isEmpty()) { + registerInBackground(); + } + } else { + Logger.e("GCMManager: No valid Google Play Services APK found."); + } + + } + + /** + * Check the device to make sure it has the Google Play Services APK. If + * it doesn't, display a dialog that allows users to download the APK from + * the Google Play Store or enable it in the device's system settings. + */ + public boolean checkPlayServices() { + int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context); + if (resultCode != ConnectionResult.SUCCESS) { +// if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { +// GooglePlayServicesUtil.getErrorDialog(resultCode, context, +// PLAY_SERVICES_RESOLUTION_REQUEST).show(); +// } else { +// Logger.e("GCMManager: This device is not supported."); +// } + Logger.e("GCMManager: This device is not supported"); + return false; + } + return true; + } + + /** + * Gets the current registration ID for application on GCM service, if there is one. + *

+ * If result is empty, the app needs to register. + * + * @return registration ID, or empty string if there is no existing + * registration ID. + */ + public String getRegistrationId() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String registrationId = prefs.getString(PROPERTY_REG_ID, ""); + if (registrationId.isEmpty()) { + Logger.e("GCMManager: Registration not found."); + return ""; + } + // Check if app was updated; if so, it must clear the registration ID + // since the existing regID is not guaranteed to work with the new + // app version. + int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE); + int currentVersion = getAppVersion(context); + if (registeredVersion != currentVersion) { + Logger.d("GCMManager: App version changed."); + return ""; + } + return registrationId; + } + + /** + * Registers the application with GCM servers asynchronously. + *

+ * Stores the registration ID and the app versionCode in the application's + * shared preferences. + */ + private void registerInBackground() { + new AsyncTask() { + @Override + protected String doInBackground(Void... params) { + String msg = ""; + try { + if (gcm == null) { + gcm = GoogleCloudMessaging.getInstance(context); + } + regid = gcm.register(SENDER_ID); + Logger.d("GCMManager: regid: "+regid); + msg = "Device registered, registration ID=" + regid; + + // Persist the regID - no need to register again. + storeRegistrationId(context, regid); + } catch (IOException ex) { + msg = "Error :" + ex.getMessage(); + // If there is an error, don't just keep trying to register. + // Require the user to click a button again, or perform + // exponential back-off. + } + return msg; + } + + + }.execute(null, null, null); + } + + /** + * Stores the registration ID and the app versionCode in the application's + * {@code SharedPreferences}. + * + * @param context application's context. + * @param regId registration ID + */ + private void storeRegistrationId(Context context, String regId) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + int appVersion = getAppVersion(context); + Logger.d("GCMManager: Saving regId on app version " + appVersion); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(PROPERTY_REG_ID, regId); + editor.putInt(PROPERTY_APP_VERSION, appVersion); + editor.commit(); + } + /** + * @return Application's version code from the {@code PackageManager}. + */ + private static int getAppVersion(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager() + .getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionCode; + } catch (NameNotFoundException e) { + // should never happen + throw new RuntimeException("Could not get package name: " + e); + } + } + + +} diff --git a/src/com/mobilyzer/gcm/GcmBroadcastReceiver.java b/src/com/mobilyzer/gcm/GcmBroadcastReceiver.java new file mode 100644 index 0000000..d264498 --- /dev/null +++ b/src/com/mobilyzer/gcm/GcmBroadcastReceiver.java @@ -0,0 +1,34 @@ +package com.mobilyzer.gcm; + +import com.mobilyzer.util.Logger; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.WakefulBroadcastReceiver; + + + +/** + * This {@code WakefulBroadcastReceiver} takes care of creating and managing a + * partial wake lock for your app. It passes off the work of processing the GCM + * message to an {@code IntentService}, while ensuring that the device does not + * go back to sleep in the transition. The {@code IntentService} calls + * {@code GcmBroadcastReceiver.completeWakefulIntent()} when it is ready to + * release the wake lock. + */ + +public class GcmBroadcastReceiver extends WakefulBroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // Explicitly specify that GcmIntentService will handle the intent. + ComponentName comp = new ComponentName(context.getPackageName(), + GcmIntentService.class.getName()); + // Start the service, keeping the device awake while it is launching. + startWakefulService(context, (intent.setComponent(comp))); + setResultCode(Activity.RESULT_OK); + Logger.d("GCMManager -> GcmBroadcastReceiver -> onReceive"); + } +} diff --git a/src/com/mobilyzer/gcm/GcmIntentService.java b/src/com/mobilyzer/gcm/GcmIntentService.java new file mode 100644 index 0000000..da3bfcd --- /dev/null +++ b/src/com/mobilyzer/gcm/GcmIntentService.java @@ -0,0 +1,85 @@ +package com.mobilyzer.gcm; + + +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.util.Logger; + +import android.app.IntentService; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; + + +/** + * This {@code IntentService} does the actual handling of the GCM message. + * {@code GcmBroadcastReceiver} (a {@code WakefulBroadcastReceiver}) holds a partial wake lock for + * this service while the service does its work. When the service is finished, it calls + * {@code completeWakefulIntent()} to release the wake lock. + */ +public class GcmIntentService extends IntentService { + public static final int NOTIFICATION_ID = 1; + NotificationCompat.Builder builder; + + public GcmIntentService() { + super("GcmIntentService"); + } + + + @Override + protected void onHandleIntent(Intent intent) { + Bundle extras = intent.getExtras(); + GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this); + // The getMessageType() intent parameter must be the intent you received + // in your BroadcastReceiver. + String messageType = gcm.getMessageType(intent); + + if (!extras.isEmpty()) { // has effect of unparcelling Bundle + /* + * Filter messages based on message type. Since it is likely that GCM will be extended in the + * future with new message types, just ignore any message types you're not interested in, or + * that you don't recognize. + */ + if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) { + Logger.d("GCMManager -> GcmIntentService -> MESSAGE_TYPE_SEND_ERROR"); + } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) { + Logger.d("GCMManager -> GcmIntentService -> MESSAGE_TYPE_DELETED"); + } else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) { + Logger.d("GCMManager -> MESSAGE_TYPE_MESSAGE -> " + extras.toString()); + if (extras.containsKey("task")) { + Logger.d("GCMManager -> " + extras.getString("task")); + + Intent newintent = new Intent(); + newintent.setAction(UpdateIntent.GCM_MEASUREMENT_ACTION); + newintent.putExtra(UpdateIntent.MEASUREMENT_TASK_PAYLOAD, extras.getString("task")); + sendBroadcast(newintent); + + } + } + } + // Release the wake lock provided by the WakefulBroadcastReceiver. + GcmBroadcastReceiver.completeWakefulIntent(intent); + } + + // // Put the message into a notification and post it. + // // This is just one simple example of what you might choose to do with + // // a GCM message. + // private void sendNotification(String msg) { + // mNotificationManager = (NotificationManager) + // this.getSystemService(Context.NOTIFICATION_SERVICE); + // + // PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + // new Intent(this, MainActivity.class), 0); + // + // NotificationCompat.Builder mBuilder = + // new NotificationCompat.Builder(this) + // .setSmallIcon(R.drawable.ic_stat_gcm) + // .setContentTitle("GCM Notification") + // .setStyle(new NotificationCompat.BigTextStyle() + // .bigText(msg)) + // .setContentText(msg); + // + // mBuilder.setContentIntent(contentIntent); + // mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + // } +} diff --git a/src/com/mobilyzer/measurements/DnsLookupTask.java b/src/com/mobilyzer/measurements/DnsLookupTask.java new file mode 100755 index 0000000..27ebe31 --- /dev/null +++ b/src/com/mobilyzer/measurements/DnsLookupTask.java @@ -0,0 +1,253 @@ +/* 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 java.io.InvalidClassException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.Date; +import java.util.Map; + +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; + +/** + * Measures the DNS lookup time + */ +public class DnsLookupTask extends MeasurementTask{ + // Type name for internal use + public static final String TYPE = "dns_lookup"; + // Human readable name for the task + public static final String DESCRIPTOR = "DNS lookup"; + + //Since it's very hard to calculate the data consumed by this task + // directly, we use a fixed value. This is on the high side. + public static final int AVG_DATA_USAGE_BYTE=2000; + + private long duration; + + /** + * The description of DNS lookup measurement + */ + public static class DnsLookupDesc extends MeasurementDesc { + public String target; + private String server; + + + public DnsLookupDesc(String key, Date startTime, Date endTime, + double intervalSec, long count, long priority, + int contextIntervalSec, Map params) { + super(DnsLookupTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + initializeParams(params); + if (this.target == null || this.target.length() == 0) { + throw new InvalidParameterException("LookupDnsTask cannot be created" + + " due to null target string"); + } + } + + /* + * @see com.google.wireless.speed.speedometer.MeasurementDesc#getType() + */ + @Override + public String getType() { + return DnsLookupTask.TYPE; + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + + this.target = params.get("target"); + this.server = params.get("server"); + } + + protected DnsLookupDesc(Parcel in) { + super(in); + target = in.readString(); + server = in.readString(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DnsLookupDesc createFromParcel(Parcel in) { + return new DnsLookupDesc(in); + } + + public DnsLookupDesc[] newArray(int size) { + return new DnsLookupDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(target); + dest.writeString(server); + } + } + + public DnsLookupTask(MeasurementDesc desc) { + super(new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, + desc.parameters)); + this.duration=Config.DEFAULT_DNS_TASK_DURATION; + } + + protected DnsLookupTask(Parcel in) { + super(in); + duration = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public DnsLookupTask createFromParcel(Parcel in) { + return new DnsLookupTask(in); + } + + public DnsLookupTask[] newArray(int size) { + return new DnsLookupTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + } + /** + * Returns a copy of the DnsLookupTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + DnsLookupDesc newDesc = new DnsLookupDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); + return new DnsLookupTask(newDesc); + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + long t1, t2; + long totalTime = 0; + InetAddress resultInet = null; + int successCnt = 0; + for (int i = 0; i < Config.DEFAULT_DNS_COUNT_PER_MEASUREMENT; i++) { + try { + DnsLookupDesc taskDesc = (DnsLookupDesc) this.measurementDesc; + Logger.i("Running DNS Lookup for target " + taskDesc.target); + t1 = System.currentTimeMillis(); + InetAddress inet = InetAddress.getByName(taskDesc.target); + t2 = System.currentTimeMillis(); + if (inet != null) { + totalTime += (t2 - t1); + resultInet = inet; + successCnt++; + } + } catch (UnknownHostException e) { + throw new MeasurementError("Cannot resovle domain name"); + } + } + + if (resultInet != null) { + Logger.i("Successfully resolved target address"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + DnsLookupTask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.COMPLETED, this.measurementDesc); + result.addResult("address", resultInet.getHostAddress()); + result.addResult("real_hostname", resultInet.getCanonicalHostName()); + result.addResult("time_ms", totalTime / successCnt); + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray= new MeasurementResult[1]; + mrArray[0]=result; + return mrArray; + + } else { + throw new MeasurementError("Cannot resovle domain name"); + } + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return DnsLookupDesc.class; + } + + @Override + public String getType() { + return DnsLookupTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public String toString() { + DnsLookupDesc desc = (DnsLookupDesc) measurementDesc; + return "[DNS Lookup]\n Target: " + desc.target + "\n Interval (sec): " + + desc.intervalSec + "\n Next run: " + desc.startTime; + } + + @Override + public boolean stop() { + //There is nothing we need to do to stop the DNS measurement + return false; + } + + @Override + public long getDuration() { + return this.duration; + } + + + @Override + public void setDuration(long newDuration) { + if(newDuration<0){ + this.duration=0; + }else{ + this.duration=newDuration; + } + } + + /** + * Since it is hard to get the amount of data sent directly, + * use a fixed value. The data consumed is usually small, and the fixed + * value is a conservative estimate. + * + * TODO find a better way to get this value + */ + @Override + public long getDataConsumed() { + return AVG_DATA_USAGE_BYTE; + } + +} diff --git a/src/com/mobilyzer/measurements/HttpTask.java b/src/com/mobilyzer/measurements/HttpTask.java new file mode 100755 index 0000000..a8ae546 --- /dev/null +++ b/src/com/mobilyzer/measurements/HttpTask.java @@ -0,0 +1,418 @@ +/* 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 CREATOR = + new Parcelable.Creator() { + 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 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 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 CREATOR = + new Parcelable.Creator() { + 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; + } +} diff --git a/src/com/mobilyzer/measurements/PageLoadTimeTask.java b/src/com/mobilyzer/measurements/PageLoadTimeTask.java new file mode 100644 index 0000000..1fdceb7 --- /dev/null +++ b/src/com/mobilyzer/measurements/PageLoadTimeTask.java @@ -0,0 +1,381 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.InvalidClassException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; + +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.PLTExecutorService; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; + + +/** + * Measures the Page Load Time + */ +public class PageLoadTimeTask extends MeasurementTask { + // Type name for internal use + public static final String TYPE = "pageloadtime"; + // Human readable name for the task + public static final String DESCRIPTOR = "Page Load Time"; + + + private long duration; + private long startTimeFilter; //used in intent filter + + private volatile ArrayList navigationTimingResults; + private volatile ArrayList resourceTimingResults; + + +// private long dataConsumed; + + /** + * The description of PageLoadTime measurement + */ + public static class PageLoadTimeDesc extends MeasurementDesc { + public String url; + public boolean spdyTest; + + + public PageLoadTimeDesc(String key, Date startTime, Date endTime, double intervalSec, + long count, long priority, int contextIntervalSec, Map params) { + super(PageLoadTimeTask.TYPE, key, startTime, endTime, intervalSec, count, priority, + contextIntervalSec, params); + initializeParams(params); + if (this.url == null) { + throw new InvalidParameterException("PageLoadTimeTask cannot be created" + + " due to null url string"); + } + } + + /* + * @see com.google.wireless.speed.speedometer.MeasurementDesc#getType() + */ + @Override + public String getType() { + return PageLoadTimeTask.TYPE; + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + + this.url = params.get("url"); + + if(params.get("spdy").equals("True")){ + this.spdyTest=true; + }else{ + this.spdyTest=false; + } + + + } + + protected PageLoadTimeDesc(Parcel in) { + super(in); + url = in.readString(); + spdyTest=in.readByte() != 0; + + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PageLoadTimeDesc createFromParcel(Parcel in) { + return new PageLoadTimeDesc(in); + } + + public PageLoadTimeDesc[] newArray(int size) { + return new PageLoadTimeDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(url); + dest.writeByte((byte) (spdyTest ? 1 : 0)); + } + } + + public PageLoadTimeTask(MeasurementDesc desc) { + super(new PageLoadTimeDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + + navigationTimingResults=new ArrayList(); + resourceTimingResults= new ArrayList(); + startTimeFilter=System.currentTimeMillis(); + +// dataConsumed=0; + + this.duration=1000*60*4; + } + + + + protected PageLoadTimeTask(Parcel in) { + super(in); + duration = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PageLoadTimeTask createFromParcel(Parcel in) { + return new PageLoadTimeTask(in); + } + + public PageLoadTimeTask[] newArray(int size) { + return new PageLoadTimeTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + } + + /** + * Returns a copy of the PLTTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + PageLoadTimeDesc newDesc = + new PageLoadTimeDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters); + return new PageLoadTimeTask(newDesc); + } + + + public synchronized ArrayList getNavigationTimingResults(){ + return navigationTimingResults; + } + + public synchronized ArrayList getResourceTimingResults(){ + return resourceTimingResults; + } + + + public synchronized boolean isDone(){ + if(((PageLoadTimeDesc)getDescription()).spdyTest){ + if(resourceTimingResults.size()>2 && navigationTimingResults.size()==2){ + return true; + } + }else{ + if(resourceTimingResults.size()>2 && navigationTimingResults.size()==1){ + return true; + } + } + + return false; + } + + + @Override + public MeasurementResult[] call() throws MeasurementError { + MeasurementResult[] mrArray = new MeasurementResult[1]; + PageLoadTimeDesc taskDesc = (PageLoadTimeDesc) this.measurementDesc; + + + Intent newintent = new Intent(PhoneUtils.getGlobalContext(), PLTExecutorService.class); + newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_URL, taskDesc.url); + newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_TEST_TYPE, taskDesc.spdyTest); + newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_STARTTIME, startTimeFilter); + PhoneUtils.getGlobalContext().startService(newintent); + Logger.d("ashkan_plt: PLT Test, Sending broadcast to start PLTExecutorService"); + + IntentFilter filter = new IntentFilter(); + filter.addAction((UpdateIntent.PLT_MEASUREMENT_ACTION)+startTimeFilter); + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + + if (intent.getAction().equals((UpdateIntent.PLT_MEASUREMENT_ACTION)+startTimeFilter)) { + Logger.d("ashkan_plt: PageLoadTimeTask: "+intent.getAction() + " RECEIVED"); + if (intent.hasExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_NAV)){ + String navigationStr=intent.getStringExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_NAV).substring(20); + navigationTimingResults.add(navigationStr); +// if(intent.hasExtra(UpdateIntent.PLT_TASK_PAYLOAD_BYTE_USED)){ +//// dataConsumed+=intent.getIntExtra(UpdateIntent.PLT_TASK_PAYLOAD_BYTE_USED, 0); +//// Logger.d("ashkan_plt: total data consumed: "+dataConsumed*2); +// } + + Logger.d("ashkan_plt: >>>>navigationTimingResults: "+navigationTimingResults.size()); + }else if(intent.hasExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_RES)){ + String resrourcesStr=intent.getStringExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_RES).substring(18); + String[] resourcesArray=resrourcesStr.split("mobilyzer_resource"); + for (String res: resourcesArray){ + if(res.length()<3){ + continue; + } + resourceTimingResults.add(res); + } + Logger.d("ashkan_plt: >>>>resourceTimingResults: "+resrourcesStr.length()+" "+resourceTimingResults.size()); + + + } + + } + } + }; + PhoneUtils.getGlobalContext().registerReceiver(broadcastReceiver, filter); + + + + for(int i=0;i<60*3;i++){ + if(isDone()){ + break; + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + Logger.e("ashkan_plt: PLTTask isDone"); + PhoneUtils.getGlobalContext().unregisterReceiver(broadcastReceiver); + + if(isDone()){ + + + Logger.i("ashkan_plt: Successfully measured PLT"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + PageLoadTimeTask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.COMPLETED, this.measurementDesc); + + if(taskDesc.spdyTest){ + result.addResult("navigationTimingResults_0", getNavigationTimingResults().get(0)); + result.addResult("navigationTimingResults_1", getNavigationTimingResults().get(1)); + int res_index=0; + for(String resResults: getResourceTimingResults()){ + result.addResult("resource_"+res_index, resResults); + res_index++; + } + }else{ + result.addResult("navigationTimingResults_0", getNavigationTimingResults().get(0)); + int res_index=0; + for(String resResults: getResourceTimingResults()){ + result.addResult("resource_"+res_index, resResults); + res_index++; + } + } + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + mrArray[0]=result; + }else{ + Logger.i("ashkan_plt: Not all the data is collected for this PLT measurement"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + PageLoadTimeTask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.FAILED, this.measurementDesc); + int nav_index=0; + for(String navResults: getNavigationTimingResults()){ + result.addResult("navigationTimingResults_"+nav_index, navResults); + nav_index++; + } + int res_index=0; + for(String resResults: getResourceTimingResults()){ + result.addResult("resource_"+res_index, resResults); + res_index++; + } + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + mrArray[0]=result; + } + + PhoneUtils.getGlobalContext().stopService(new Intent(PhoneUtils.getGlobalContext(), PLTExecutorService.class)); + return mrArray; + + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return PageLoadTimeDesc.class; + } + + @Override + public String getType() { + return PageLoadTimeTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public String toString() { + return null; + } + + @Override + public boolean stop() { + // There is nothing we need to do to stop the PLT measurement + return false; + } + + @Override + public long getDuration() { + return this.duration; + } + + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + + /** + * Since it is hard to get the amount of data sent directly, use a fixed value. The data consumed + * is usually small, and the fixed value is a conservative estimate. + * + * TODO find a better way to get this value + */ + @Override + public long getDataConsumed() { + long avgTotalPageSize=1024*1024; + if (((PageLoadTimeDesc)getDescription()).spdyTest){ + return 2*avgTotalPageSize; + }else{ + return avgTotalPageSize; + } + } +} diff --git a/src/com/mobilyzer/measurements/ParallelTask.java b/src/com/mobilyzer/measurements/ParallelTask.java new file mode 100644 index 0000000..bef5df7 --- /dev/null +++ b/src/com/mobilyzer/measurements/ParallelTask.java @@ -0,0 +1,248 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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 java.io.InvalidClassException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; + + +/** + * + * @author Ashkan Nikravesh (ashnik@umich.edu) + * + * Parallel Task is a measurement task that can execute more than one measurement task + * in parallel using a thread pool. + */ +public class ParallelTask extends MeasurementTask{ + + private long duration; + private List tasks; + + private ExecutorService executor; + + // Type name for internal use + public static final String TYPE = "parallel"; + // Human readable name for the task + public static final String DESCRIPTOR = "parallel"; + + private long dataConsumed; + + + public static class ParallelDesc extends MeasurementDesc { + + public ParallelDesc(String key, Date startTime, + Date endTime, double intervalSec, long count, long priority, + int contextIntervalSec, Map params) + throws InvalidParameterException { + super(ParallelTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + // initializeParams(params); + + } + + @Override + protected void initializeParams(Map params) { + } + + @Override + public String getType() { + return ParallelTask.TYPE; + } + + protected ParallelDesc(Parcel in) { + super(in); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ParallelDesc createFromParcel(Parcel in) { + return new ParallelDesc(in); + } + + public ParallelDesc[] newArray(int size) { + return new ParallelDesc[size]; + } + }; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return ParallelDesc.class; + } + + + + public ParallelTask(MeasurementDesc desc, ArrayList tasks) { + super(new ParallelDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + this.tasks=(List) tasks.clone(); + long maxduration=0; + for(MeasurementTask mt: tasks){ + if(mt.getDuration()>maxduration){ + maxduration=mt.getDuration(); + } + } + this.duration=maxduration; + this.dataConsumed=0; + + } + + protected ParallelTask(Parcel in) { + super(in); +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + // we cannot directly cast Parcelable[] to MeasurementTask[]. Cast them one-by-one + Parcelable[] tempTasks = in.readParcelableArray(MeasurementTask.class.getClassLoader()); + tasks = new ArrayList(); + long maxDuration = 0; + for ( Parcelable pTask : tempTasks ) { + MeasurementTask mt = (MeasurementTask) pTask; + tasks.add(mt); + if (mt.getDuration() > maxDuration) { + maxDuration = mt.getDuration(); + } + } + this.duration = maxDuration; + this. dataConsumed = 0; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public ParallelTask createFromParcel(Parcel in) { + return new ParallelTask(in); + } + + public ParallelTask[] newArray(int size) { + return new ParallelTask[size]; + } + }; + + @Override + public int describeContents() { + return super.describeContents(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelableArray(tasks.toArray(new MeasurementTask[tasks.size()]), flags); + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + for (MeasurementTask task: this.tasks){ + task.setContext(this.getContext()); + } //added by Clarence + long timeout=duration; + executor=Executors.newFixedThreadPool(this.tasks.size()); + + if(timeout==0){ + timeout=Config.DEFAULT_PARALLEL_TASK_DURATION; + }else{ + //this is the longest time a task can run before it is forcibly killed + timeout*=2; + } + ArrayList allResults=new ArrayList(); + List> futures; + try { + futures=executor.invokeAll(this.tasks,timeout,TimeUnit.MILLISECONDS); + for(Future f: futures){ + MeasurementResult[] r=f.get(); + for(int i=0;i newTaskList=new ArrayList(); + for(MeasurementTask mt: tasks){ + newTaskList.add(mt.clone()); + } + return new ParallelTask(newDesc,newTaskList); + } + + @Override + public boolean stop() { + return false; + } + + @Override + public long getDuration() { + return duration; + } + + @Override + public void setDuration(long newDuration) { + if(newDuration<0){ + this.duration=0; + }else{ + this.duration=newDuration; + } + } + + public MeasurementTask[] getTasks() { + return tasks.toArray(new MeasurementTask[tasks.size()]); + } + + //TODO + @Override + public long getDataConsumed() { + return dataConsumed; + } +} diff --git a/src/com/mobilyzer/measurements/PingTask.java b/src/com/mobilyzer/measurements/PingTask.java new file mode 100755 index 0000000..ed1e65c --- /dev/null +++ b/src/com/mobilyzer/measurements/PingTask.java @@ -0,0 +1,679 @@ +/* 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.content.Context; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementScheduler; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.Util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InvalidClassException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; + +/** + * A callable that executes a ping task using one of three methods + */ +public class PingTask extends MeasurementTask{ + + // Type name for internal use + public static final String TYPE = "ping"; + // Human readable name for the task + public static final String DESCRIPTOR = "ping"; + /* Default payload size of the ICMP packet, plus the 8-byte ICMP header resulting in a total of + * 64-byte ICMP packet */ + public static final int DEFAULT_PING_PACKET_SIZE = 56; + public static final int DEFAULT_PING_TIMEOUT = 10; + public static final int DEFAULT_PING_TTL = 51; + + private long duration; + + private Process pingProc = null; + private String PING_METHOD_CMD = "ping_cmd"; + private String PING_METHOD_JAVA = "java_ping"; + private String PING_METHOD_HTTP = "http"; + private String targetIp = null; + //Track data consumption for this task to avoid exceeding user's limit + private long dataConsumed; + + + private Context context = null; // added by Clarence + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, Context context) { + this.context = context; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.context.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } + + /** + * Encode ping specific parameters, along with common parameters inherited from MeasurmentDesc + * @author wenjiezeng@google.com (Steve Zeng) + * + */ + public static class PingDesc extends MeasurementDesc { + public String pingExe = null; + // Host address either in the numeric form or domain names + public String target = null; + // The payload size in bytes of the ICMP packet + public int packetSizeByte = PingTask.DEFAULT_PING_PACKET_SIZE; + public int pingTimeoutSec = PingTask.DEFAULT_PING_TIMEOUT; + public int pingTimeToLive= PingTask.DEFAULT_PING_TTL; + public double pingIcmpIntervalSec= Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC; + + + public PingDesc(String key, Date startTime, + Date endTime, double intervalSec, long count, long priority, int contextIntervalSec, + Map params) throws InvalidParameterException { + super(PingTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec,params); + initializeParams(params); + if (this.target == null || this.target.length() == 0) { + throw new InvalidParameterException("PingTask cannot be created due " + + " to null target string"); + } + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + + this.target = params.get("target"); + + try { + String val = null; + if ((val = params.get("packet_size_byte")) != null && val.length() > 0 && + Integer.parseInt(val) > 0) { + this.packetSizeByte = Integer.parseInt(val); + } + if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0 && + Integer.parseInt(val) > 0) { + this.pingTimeoutSec = Integer.parseInt(val); + } + if ((val = params.get("ttl")) != null && val.length() > 0 && + Integer.parseInt(val) > 0) { + this.pingTimeToLive = Integer.parseInt(val); + } + if ((val = params.get("icmp_interval_sec")) != null && val.length() > 0 && + Double.parseDouble(val) > 0) { + this.pingIcmpIntervalSec = Double.parseDouble(val); + } + } catch (NumberFormatException e) { + throw new InvalidParameterException("PingTask cannot be created due to invalid params"); + } + } + + @Override + public String getType() { + return PingTask.TYPE; + } + + protected PingDesc(Parcel in) { + super(in); + pingExe = in.readString(); + target = in.readString(); + packetSizeByte = in.readInt(); + pingTimeoutSec = in.readInt(); + pingTimeToLive = in.readInt(); + pingIcmpIntervalSec = in.readDouble(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public PingDesc createFromParcel(Parcel in) { + return new PingDesc(in); + } + + public PingDesc[] newArray(int size) { + return new PingDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(pingExe); + dest.writeString(target); + dest.writeInt(packetSizeByte); + dest.writeInt(pingTimeoutSec); + dest.writeInt(pingTimeToLive); + dest.writeDouble(pingIcmpIntervalSec); + } + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return PingDesc.class; + } + + public PingTask(MeasurementDesc desc) { + super(new PingDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + this.duration=Config.PING_COUNT_PER_MEASUREMENT*500; + // this.taskProgress=TaskProgress.FAILED; + // this.stopFlag=false; + this.dataConsumed=0; + } + + protected PingTask(Parcel in) { + super(in); + duration = in.readLong(); + dataConsumed = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PingTask createFromParcel(Parcel in) { + return new PingTask(in); + } + + public PingTask[] newArray(int size) { + return new PingTask[size]; + } + }; + + @Override + public int describeContents() { + return super.describeContents(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + dest.writeLong(dataConsumed); + } + /** + * Returns a copy of the PingTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + PingDesc newDesc = new PingDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters); + return new PingTask(newDesc); + } + + /* We will use three methods to ping the requested resource in the order of PING_COMMAND, + * JAVA_ICMP_PING, and HTTP_PING. If all fails, then we declare the resource unreachable */ + @Override + public MeasurementResult[] call() throws MeasurementError { + MeasurementResult[] result = null; + PingDesc desc = (PingDesc) measurementDesc; + int ipByteLength; + try { + InetAddress addr = InetAddress.getByName(desc.target); + // Get the address length + ipByteLength = addr.getAddress().length; + Logger.i("IP address length is " + ipByteLength); + // All ping methods ping against targetIp rather than desc.target + targetIp = addr.getHostAddress(); + Logger.i("IP is " + targetIp); + } catch (UnknownHostException e) { + throw new MeasurementError("Unknown host " + desc.target); + } + result=new MeasurementResult[1]; + try { + Logger.i("running ping command"); + // Prevents the phone from going to low-power mode where WiFi turns off + result[0]=executePingCmdTask(ipByteLength); + return result; + } + + catch (MeasurementError e) { + try { + Logger.i("running java ping"); + result[0]=executeJavaPingTask(); + return result; + } catch (MeasurementError ee) { + Logger.i("running http ping"); + result[0]=executeHttpPingTask(); + return result; + } + } + } + + @Override + public String getType() { + return PingTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + private MeasurementResult constructResult(ArrayList rrtVals, double packetLoss, + int packetsSent, String pingMethod) { + double min = Double.MAX_VALUE; + double max = Double.MIN_VALUE; + double mdev, avg, filteredAvg; + double total = 0; + + if (rrtVals.size() == 0) { + return null; + } + + for (double rrt : rrtVals) { + if (rrt < min) { + min = rrt; + } + if (rrt > max) { + max = rrt; + } + total += rrt; + } + + avg = total / rrtVals.size(); + mdev = Util.getStandardDeviation(rrtVals, avg); + filteredAvg = filterPingResults(rrtVals, avg); + + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + + MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + PingTask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.COMPLETED, this.measurementDesc); + + result.addResult("target_ip", targetIp); + result.addResult("mean_rtt_ms", avg); + result.addResult("min_rtt_ms", min); + result.addResult("max_rtt_ms", max); + result.addResult("stddev_rtt_ms", mdev); + if (filteredAvg != avg) { + result.addResult("filtered_mean_rtt_ms", filteredAvg); + } + result.addResult("packet_loss", packetLoss); + result.addResult("packets_sent", packetsSent); + result.addResult("ping_method", pingMethod); + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + return result; + } + + private void cleanUp(Process proc) { + try { + if (proc != null) { + proc.destroy(); + } + } catch (Exception e) { + Logger.w("Unable to kill ping process" + e.getMessage()); + } + } + + /** + * Compute the average of the filtered rtts. + * The first several ping results are usually extremely large as the device + * needs to activate the wireless interface and resolve domain names. + * Such distorted measurements are filtered out + */ + private double filterPingResults(final ArrayList rrts, double avg) { + double rrtAvg = avg; + // Our # of results should be less than the # of times we ping + try { + ArrayList filteredResults = Util.applyInnerBandFilter(rrts, + Double.MIN_VALUE, rrtAvg * Config.PING_FILTER_THRES); + // Now we compute the average again based on the filtered results + if (filteredResults != null && filteredResults.size() > 0) { + rrtAvg = Util.getSum(filteredResults) / filteredResults.size(); + } + } catch (InvalidParameterException e) { + Log.wtf("", "This should never happen because rrts is never empty"); + } + return rrtAvg; + } + + // Runs when SystemState is IDLE + private MeasurementResult executePingCmdTask(int ipByteLen) + throws MeasurementError { + Logger.i("Starting executePingCmdTask"); + PingDesc pingTask = (PingDesc) this.measurementDesc; + String errorMsg = ""; + MeasurementResult measurementResult = null; + + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; + + // TODO(Wenjie): Add a exhaustive list of ping locations for different + // Android phones + pingTask.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen); + Logger.i("Ping executable is " + pingTask.pingExe); + if (pingTask.pingExe == null) { + Logger.e("Ping executable not found"); + throw new MeasurementError("Ping executable not found"); + } + try { + String command = Util.constructCommand(pingTask.pingExe, "-i", + pingTask.pingIcmpIntervalSec, + "-s", pingTask.packetSizeByte, "-w", pingTask.pingTimeoutSec, "-c", + Config.PING_COUNT_PER_MEASUREMENT, "-t" ,pingTask.pingTimeToLive, targetIp); + Logger.i("Running: " + command); + pingProc = Runtime.getRuntime().exec(command); + + dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; + + // Grab the output of the process that runs the ping command + InputStream is = pingProc.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + + String line = null; + int lineCnt = 0; + ArrayList rrts = new ArrayList(); + ArrayList receivedIcmpSeq = new ArrayList(); + double packetLoss = Double.MIN_VALUE; + int packetsSent = Config.PING_COUNT_PER_MEASUREMENT; + + // Process each line of the ping output and store the rrt in array rrts. + while ((line = br.readLine()) != null) { + // Ping prints a number of 'param=value' pairs, among which we only need + // the 'time=rrt_val' pair + String[] extractedValues = Util.extractInfoFromPingOutput(line); + + if (extractedValues != null) { + int curIcmpSeq = Integer.parseInt(extractedValues[0]); + double rrtVal = Double.parseDouble(extractedValues[1]); + + // ICMP responses from the system ping command could be duplicate + // and out of order + if (!receivedIcmpSeq.contains(curIcmpSeq)) { + rrts.add(rrtVal); + receivedIcmpSeq.add(curIcmpSeq); + } + } + + + // Get the number of sent/received pings from the ping command output + int[] packetLossInfo = Util.extractPacketLossInfoFromPingOutput(line); + if (packetLossInfo != null) { + packetsSent = packetLossInfo[0]; + int packetsReceived = packetLossInfo[1]; + packetLoss = 1 - ((double) packetsReceived / (double) packetsSent); + } + this.context = this.getContext(); + if ( this.context != null ){ + if (rrts.size() >= 2 && (rrts.size() < Config.PING_COUNT_PER_MEASUREMENT) && (extractedValues != null) ){ + IM_measurementResult = constructResult(rrts,packetLoss, + rrts.size(),PING_METHOD_CMD); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.context); + + + } + } + //IM_packetsSent++; + + Logger.i(line); + } + // Use the output from the ping command to compute packet loss. If that's not + // available, use an estimation. + if (packetLoss == Double.MIN_VALUE) { + packetLoss = 1 - + ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + } + measurementResult = constructResult(rrts, packetLoss, + packetsSent, PING_METHOD_CMD); + } catch (IOException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } catch (SecurityException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } catch (NumberFormatException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } catch (InvalidParameterException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } finally { + // All associated streams with the process will be closed upon destroy() + cleanUp(pingProc); + } + + if (measurementResult == null) { + Logger.e("Error running ping: " + errorMsg); + throw new MeasurementError(errorMsg); + } + return measurementResult; + } + + // Runs when the ping command fails + private MeasurementResult executeJavaPingTask() throws MeasurementError { + PingDesc pingTask = (PingDesc) this.measurementDesc; + long pingStartTime = 0; + long pingEndTime = 0; + ArrayList rrts = new ArrayList(); + String errorMsg = ""; + MeasurementResult result = null; + double packetLoss = Double.MIN_VALUE; //added by Clarence + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; + + try { + int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec / + Config.PING_COUNT_PER_MEASUREMENT); + int successfulPingCnt = 0; + long totalPingDelay = 0; + for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) { + pingStartTime = System.currentTimeMillis(); + boolean status = InetAddress.getByName(targetIp).isReachable(timeOut); + pingEndTime = System.currentTimeMillis(); + long rrtVal = pingEndTime - pingStartTime; + if (status) { + totalPingDelay += rrtVal; + rrts.add((double) rrtVal); + packetLoss = 1 - + ((double) rrts.size() / (i+1)); + dataConsumed += pingTask.packetSizeByte * (i+1) * 2; + this.context = this.getContext(); + if ( this.context != null){ + IM_measurementResult = constructResult(rrts,packetLoss, + (i+1),PING_METHOD_JAVA); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.context); + + } + + + } + } + Logger.i("java ping succeeds"); +// double packetLoss = 1 - +// ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); //deleted by Clarence + if (packetLoss == Double.MIN_VALUE) { + packetLoss = 1 - + ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + } + + dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; + + result = constructResult(rrts, packetLoss, + Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_JAVA); + } catch (IllegalArgumentException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } catch (IOException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } + if (result != null) { + return result; + } else { + Logger.i("java ping fails"); + throw new MeasurementError(errorMsg); + } + } + + /** + * Use the HTTP Head method to emulate ping. The measurement from this method + * can be substantially (2x) greater than the first two methods and inaccurate. + * This is because, depending on the implementing of the destination web + * server, either a quick HTTP response is replied or some actual heavy + * lifting will be done in preparing the response + * */ + private MeasurementResult executeHttpPingTask() throws MeasurementError { + long pingStartTime = 0; + long pingEndTime = 0; + ArrayList rrts = new ArrayList(); + PingDesc pingTask = (PingDesc) this.measurementDesc; + String errorMsg = ""; + MeasurementResult result = null; + double packetLoss = Double.MIN_VALUE; //added by Clarence + MeasurementResult IM_measurementResult = null; //added by Clarence + MeasurementResult[] IM_result = null; + IM_result = new MeasurementResult[1]; + + try { + long totalPingDelay = 0; + + URL url = new URL("http://"+ pingTask.target); + + int timeOut = (int) (3000 * (double) pingTask.pingTimeoutSec / + Config.PING_COUNT_PER_MEASUREMENT); + + for (int i = 0; i < Config.PING_COUNT_PER_MEASUREMENT; i++) { + pingStartTime = System.currentTimeMillis(); + HttpURLConnection httpClient = (HttpURLConnection) url.openConnection(); + httpClient.setRequestProperty("Connection", "close"); + httpClient.setRequestMethod("HEAD"); + httpClient.setReadTimeout(timeOut); + httpClient.setConnectTimeout(timeOut); + httpClient.connect(); + pingEndTime = System.currentTimeMillis(); + httpClient.disconnect(); + rrts.add((double) (pingEndTime - pingStartTime)); + packetLoss = 1 - + ((double) rrts.size() / (i+1)); + dataConsumed += pingTask.packetSizeByte * (i+1) * 2; + this.context = this.getContext(); + if ( this.context != null){ + IM_measurementResult = constructResult(rrts,packetLoss, + (i+1),PING_METHOD_HTTP); + IM_result[0] = IM_measurementResult; + broadcastIntermediateMeasurement(IM_result,this.context); + + } + + } + Logger.i("HTTP get ping succeeds"); + Logger.i("RTT is " + rrts.toString()); +// double packetLoss = 1 +// - ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + if (packetLoss == Double.MIN_VALUE) { + packetLoss = 1 - + ((double) rrts.size() / (double) Config.PING_COUNT_PER_MEASUREMENT); + } + + dataConsumed += pingTask.packetSizeByte * Config.PING_COUNT_PER_MEASUREMENT * 2; + + result = constructResult(rrts, packetLoss, + Config.PING_COUNT_PER_MEASUREMENT, PING_METHOD_HTTP); + } catch (MalformedURLException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } catch (IOException e) { + Logger.e(e.getMessage()); + errorMsg += e.getMessage() + "\n"; + } + if (result != null) { + return result; + } else { + Logger.i("HTTP get ping fails"); + throw new MeasurementError(errorMsg); + } + } + + @Override + public String toString() { + PingDesc desc = (PingDesc) measurementDesc; + return "[Ping]\n Target: " + desc.target + "\n Interval (sec): " + + desc.intervalSec + "\n Next run: " + desc.startTime; + } + + @Override + public boolean stop() { + return false; + } + + @Override + public long getDuration() { + return this.duration; + } + + @Override + public void setDuration(long newDuration) { + if(newDuration<0){ + this.duration=0; + }else{ + this.duration=newDuration; + } + } + + /** + * Data sent so far by this task. + * + * We count packets sent directly to calculate the data sent + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } + +} diff --git a/src/com/mobilyzer/measurements/RRCTask.java b/src/com/mobilyzer/measurements/RRCTask.java new file mode 100644 index 0000000..2307d94 --- /dev/null +++ b/src/com/mobilyzer/measurements/RRCTask.java @@ -0,0 +1,1849 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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 java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.InvalidClassException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; + +import com.mobilyzer.Checkin; +import com.mobilyzer.Config; +import com.mobilyzer.DeviceInfo; +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 android.content.SharedPreferences; +import android.net.TrafficStats; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.StringBuilderPrinter; + + + +/** + * This class measures the round trip times of packets as you vary the packet timings between them, + * with the purpose of inferring RRC state transitions. + * + * See "Characterizing Radio Resource Allocation for 3G Networks" by Feng et. al, IMC 2010 for a + * full explanation of the methodology and goals. + * + * TODO (sanae): Pause the RRC tasks when a checkin is performed, rather than pausing the checkin + * while the RRC task is performed. + * + * @author sanae@umich.edu (Sanae Rosen) + * + */ +public class RRCTask extends MeasurementTask { + // Type name for internal use + public static final String TYPE = "rrc"; + // Human readable name for the task + public static final String DESCRIPTOR = "rrc"; + public static String TAG = "MobiPerf_RRC_INFERENCE"; + + private volatile boolean stopFlag; + private long duration; + + //Track data consumption for this task to avoid exceeding user's limit + public static long data_consumed = 0; + + + /** + * Stores parameters for the RRC inference task + * + * @author sanae@umich.edu (Sanae Rosen) + * + */ + public static class RRCDesc extends MeasurementDesc { + private static String HOST = "www.google.com"; + + // Default echo server name and port to measure the RTT to infer RRC state + private static int PORT = 50000; + private static String ECHO_HOST = "ep2.eecs.umich.edu"; + // Perform RTT measurements every GRANULARITY ms + public int GRANULARITY = 500; + // MIN / MAX is the echo packet size + int MIN = 0; + int MAX = 1024; + // Default total number of measurements + int size = 31; + // Echo server / port, and target to perform the upper-layer tasks + public String echoHost = ECHO_HOST; + public String target = HOST; + int port = PORT; + + long testId; // unique value for this set of tests + + // Default threshold to repeat each RTT measurement because of background traffic + int GIVEUP_THRESHHOLD = 15; + + // server controled variable + boolean DNS = true; + boolean TCP = true; + boolean HTTP = true; + boolean RRC = true; + boolean SIZES = true; + + // Whether RRC result is visible to users + public boolean RESULT_VISIBILITY = false; + + /* + * For the upper-layer tests, a series of tests are made for different inter-packet intervals, + * in order. "Times" indicates the inter-packet intervals, the other fields store the results. + * All must be the same size. + */ + Integer[] times; // The times where the above tests were made, in units of GRANULARITY. + int sizeGranularity = 200; // the spacing between sizes to test + int[] httpTest; // The results of the HTTP test performed at each time + int[] dnsTest; // likewise, for the DNS test + int[] tcpTest; // likewise, for the TCP test + + // Whether or not to run the upper layer tests, i.e. the HTTP, TCP and DNS tests. + // Disabling this flag will disable all upper layer tests. + private boolean runUpperLayerTests = false; + + /* + * Default times between packets for which the upper layer tests are performed. Later, these + * times will be replaced by times from the model. These times are GRANULARITY milliseconds: if + * GRANULARITY is 500, then a default time of 6 means measurements are taken 500 ms apart. + */ + Integer[] defaultTimesULTasks = new Integer[] {0, 2, 4, 8, 12, 16, 22}; + + + public RRCDesc(String key, Date startTime, Date endTime, double intervalSec, long count, + long priority, int contextIntervalSec, Map params) { + super(RRCTask.TYPE, key, startTime, endTime, intervalSec, count, priority, + contextIntervalSec, params); + initializeParams(params); + } + + protected RRCDesc(Parcel in) { + super(in); +// ClassLoader loader = Thread.currentThread().getContextClassLoader(); + echoHost = in.readString(); + target = in.readString(); + MIN = in.readInt(); + MAX = in.readInt(); + port = in.readInt(); + size = in.readInt(); + sizeGranularity = in.readInt(); + DNS = in.readByte() != 0; + HTTP = in.readByte() != 0; + TCP = in.readByte() != 0; + RRC = in.readByte() != 0; + SIZES = in.readByte() != 0; + RESULT_VISIBILITY = in.readByte() != 0; + GIVEUP_THRESHHOLD = in.readInt(); + Object[] temp = in.readArray(Integer.class.getClassLoader()); + times = Arrays.copyOf(temp, temp.length, Integer[].class); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + @Override + public RRCDesc createFromParcel(Parcel in) { + return new RRCDesc(in); + } + + @Override + public RRCDesc[] newArray(int size) { + return new RRCDesc[size]; + } + + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(echoHost); + dest.writeString(target); + dest.writeInt(MIN); + dest.writeInt(MAX); + dest.writeInt(port); + dest.writeInt(size); + dest.writeInt(sizeGranularity); + dest.writeByte((byte) (DNS ? 1 : 0)); + dest.writeByte((byte) (HTTP ? 1 : 0)); + dest.writeByte((byte) (TCP ? 1 : 0)); + dest.writeByte((byte) (RRC ? 1 : 0)); + dest.writeByte((byte) (SIZES ? 1 : 0)); + dest.writeByte((byte) (RESULT_VISIBILITY ? 1 : 0)); + dest.writeInt(GIVEUP_THRESHHOLD); + dest.writeArray(times); + } + + public MeasurementResult getResults(MeasurementResult result) { + if (HTTP) result.addResult("http", httpTest); + if (TCP) result.addResult("tcp", tcpTest); + if (DNS) result.addResult("dns", dnsTest); + result.addResult("times", times); + return result; + } + + public void displayResults(StringBuilderPrinter printer) { + String DEL = "\t", toprint = DEL + DEL; + for (int i = 1; i <= times.length; i++) { + toprint += DEL + " | state" + i; + } + toprint += " |"; + int oneLineLen = toprint.length(); + toprint += "\n"; + // seperator + for (int i = 0; i < oneLineLen; i++) { + toprint += "-"; + } + toprint += "\n"; + if (HTTP) { + toprint += "HTTP (ms)" + DEL; + for (int i = 0; i < httpTest.length; i++) { + toprint += DEL + " | " + Integer.toString(httpTest[i]); + } + toprint += " |\n"; + for (int i = 0; i < oneLineLen; i++) { + toprint += "-"; + } + toprint += "\n"; + } + + if (DNS) { + toprint += "DNS (ms)" + DEL; + for (int i = 0; i < dnsTest.length; i++) { + toprint += DEL + " | " + Integer.toString(dnsTest[i]); + } + toprint += " |\n"; + for (int i = 0; i < oneLineLen; i++) { + toprint += "-"; + } + toprint += "\n"; + } + + if (TCP) { + toprint += "TCP (ms)" + DEL; + for (int i = 0; i < tcpTest.length; i++) { + toprint += DEL + " | " + Integer.toString(tcpTest[i]); + } + toprint += " |\n"; + for (int i = 0; i < oneLineLen; i++) { + toprint += "-"; + } + toprint += "\n"; + } + + toprint += "Timers (s)"; + for (int i = 0; i < times.length; i++) { + double curTime = (double) times[i] * (double) GRANULARITY / 1000.0; + toprint += DEL + " | " + String.format("%.2f", curTime); + } + toprint += " |\n"; + printer.println(toprint); + } + + @Override + public String getType() { + return RRCTask.TYPE; + } + + /** + * Given the parameters fetched from the server, sets up the parameters as needed for the upper + * layer tests, i.e. the application layer tests. + */ + @Override + protected void initializeParams(Map params) { + + // In this case, we fall back to the default values defined above. + if (params == null) { + return; + } + + // The parameters for the echo server + this.echoHost = params.get("echo_host"); + this.target = params.get("target"); + if (this.echoHost == null) { + this.echoHost = ECHO_HOST; + } + if (this.target == null) { + this.target = HOST; + } + Logger.d("param: echo_host " + this.echoHost); + Logger.d("param: target " + this.target); + + try { + String val = null; + // Size of the small packet + if ((val = params.get("min")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { + this.MIN = Integer.parseInt(val); + } + Logger.d("param: Min " + this.MIN); + // Size of the large packet + if ((val = params.get("max")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { + this.MAX = Integer.parseInt(val); + } + Logger.d("param: MAX " + this.MAX); + // Echo server port + if ((val = params.get("port")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { + this.port = Integer.parseInt(val); + } + Logger.d("param: port " + this.port); + // Number of tests to run from the RRC test + if ((val = params.get("size")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { + this.size = Integer.parseInt(val); + } + Logger.d("param: size " + this.size); + // When testing size dependence, increase the + if ((val = params.get("size_granularity")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.sizeGranularity = Integer.parseInt(val); + } + Logger.d("param: size_granularity " + this.sizeGranularity); + // Whether or not to run the DNS test + if ((val = params.get("dns")) != null && val.length() > 0) { + this.DNS = Boolean.parseBoolean(val); + } + Logger.d("param: DNS " + this.DNS); + // Whether or not to run the HTTP test + if ((val = params.get("http")) != null && val.length() > 0) { + this.HTTP = Boolean.parseBoolean(val); + } + Logger.d("param: HTTP " + this.HTTP); + // Whether or not to run the TCP test + if ((val = params.get("tcp")) != null && val.length() > 0) { + this.TCP = Boolean.parseBoolean(val); + } + Logger.d(params.get("rrc")); + Logger.d("param: TCP " + this.TCP); + // Whether or not to run the RRC inference task + if ((val = params.get("rrc")) != null && val.length() > 0) { + this.RRC = Boolean.parseBoolean(val); + } + Logger.d("param: RRC " + this.RRC); + if ((val = params.get("measure_sizes")) != null && val.length() > 0) { + this.SIZES = Boolean.parseBoolean(val); + } + Logger.d("param: SIZES " + this.SIZES); + // Whether the RRC result is visible to users + if ((val = params.get("result_visibility")) != null && val.length() > 0) { + this.RESULT_VISIBILITY = Boolean.parseBoolean(val); + } + Logger.d("param: visibility " + this.RESULT_VISIBILITY); + // How many times to retry a test when interrupted by background traffic + if ((val = params.get("giveup_threshhold")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.GIVEUP_THRESHHOLD = Integer.parseInt(val); + } + Logger.d("param: GIVEUP_THRESHHOLD " + this.GIVEUP_THRESHHOLD); + + // Default assumed timers for the upper layer tests (HTTP, DNS, TCP), + // in units of GRANULARITY. These are set via a comma-separated list + // of numbers. + if ((val = params.get("default_extra_test_timers")) != null && val.length() > 0) { + String[] timesString = val.split("\\s*,\\s*"); + List stringList = new ArrayList(Arrays.asList(timesString)); + List intList = new ArrayList(); + Iterator iterator = stringList.iterator(); + while (iterator.hasNext()) { + intList.add(Integer.parseInt(iterator.next())); + } + times = (Integer[]) intList.toArray(new Integer[intList.size()]); + + } + if (times == null) { + times = defaultTimesULTasks; + } + } catch (NumberFormatException e) { + throw new InvalidParameterException(" RRCTask cannot be created due to invalid params"); + } + + if (size == 0) { + // 31 tests, by default. From 0s to 15s inclusive, in half-second intervals. + size = 31; + } + } + + /** + * For the arrays holding the results for the upper layer tests, we need to initialize them to + * be the same size as the number of tests we run. -1 means uninitialized. + * + * @param size Number of results to store + */ + public void initializeExtraTaskResults(int size) { + httpTest = new int[size]; + dnsTest = new int[size]; + tcpTest = new int[size]; + for (int i = 0; i < size; i++) { + httpTest[i] = -1; + dnsTest[i] = -1; + tcpTest[i] = -1; + } + runUpperLayerTests = true; + } + + /** + * Given an interpacket interval and a round trip time from an HTTP test, store that value. + * + * @param index Index into measurement result array, corresponding to an interpacket interval + * @param rtt Time for HTTP test to complete, in milliseconds + * @throws MeasurementError + */ + public void setHttp(int index, int rtt) throws MeasurementError { + if (!runUpperLayerTests) { + throw new MeasurementError("Data class not initialized"); + } + httpTest[index] = rtt; + } + + /** + * Given an interpacket interval and a round trip time from a TCP test, store that value. + * + * @param index Index into measurement result array, corresponding to an interpacket interval + * @param rtt Time for the TCP test to complete, in milliseconds + * @throws MeasurementError + */ + public void setTcp(int index, int rtt) throws MeasurementError { + if (!runUpperLayerTests) { + throw new MeasurementError("Data class not initialized"); + } + tcpTest[index] = rtt; + } + + /** + * Given an interpacket interval and a round trip time from a DNS test, store that value. + * + * @param index Index into measurement result array, corresponding to an interpacket interval + * @param rtt Time for the DNS test to complete, in milliseconds + * @throws MeasurementError + */ + public void setDns(int index, int rtt) throws MeasurementError { + if (!runUpperLayerTests) { + throw new MeasurementError("Data class not initialized"); + } + dnsTest[index] = rtt; + } + + } + + /** + * Stores data from the tests on the effect of packet size on RRC state related delays + * + * @author sanae@umich.edu (Sanae Rosen) + */ + public static class RrcSizeTestData { + int time; // Interval between packets + int size; // Size of packets sent + long result; // Round trip time, in milliseconds + long testId; // A unique id associated with a set of measurements + + /** + * + * @param time The inter-packet interval + * @param size The packet size, in bytes + * @param result The round trip time for that packet size and inter-packet interval + * @param testId The unique id for this set of tests + */ + public RrcSizeTestData(int time, int size, long result, long testId) { + this.time = time; + this.size = size; + this.result = result; + this.testId = testId; + } + + public JSONObject toJSON(String networktype, String phoneId) throws JSONException { + JSONObject entry = new JSONObject(); + entry.put("network_type", networktype); + entry.put("phone_id", phoneId); + entry.put("test_id", testId); + entry.put("time_delay", time); + entry.put("size", size); + entry.put("result", result); + + return entry; + } + + } + + /** + * Store the results of our RRC test. + * + * Data is stored as a list of results indexed by the test number. Tests are performed in order + * with increasing inter-packet intervals and results and data about the tests are stored here. + * + * @author sanae@umich.edu (Sanae Rosen) + * + */ + public static class RRCTestData { + + // Round-trip times, in ms + int[] rttsSmall; + int[] rttsLarge; + // Packets lost for each test + int[] packetsLostSmall; + int[] packetsLostLarge; + // Signal strengths at time of each test + int[] signalStrengthSmall; + int[] signalStrengthLarge; + // Error Counts from each test + int[] errorCountSmall; + int[] errorCountLarge; + + ArrayList packetSizes; + + // Unique incrementing value that identifies this set of tests. + long testId; + + /** + * @param size Number of tests to run (each corresponding to an interpacket interval, increasing + * in increments of 500 ms) + * @param testId Unique ID for this set of tests + */ + public RRCTestData(int size, long testId) { + size = size + 1; + + + rttsSmall = new int[size]; + rttsLarge = new int[size]; + packetsLostSmall = new int[size]; + packetsLostLarge = new int[size]; + signalStrengthSmall = new int[size]; + signalStrengthLarge = new int[size]; + errorCountLarge = new int[size]; + errorCountSmall = new int[size]; + + this.testId = testId; + + packetSizes = new ArrayList(); + + // Set default values + for (int i = 0; i < rttsSmall.length; i++) { + // 7000 is the cutoff for timeouts. + // This makes the model-building script treat no data and timeouts the same. + rttsSmall[i] = 7000; + rttsLarge[i] = 7000; + packetsLostSmall[i] = -1; + packetsLostLarge[i] = -1; + signalStrengthSmall[i] = -1; + signalStrengthLarge[i] = -1; + errorCountSmall[i] = -1; + errorCountLarge[i] = -1; + } + } + + public long testId() { + return testId; + } + + public String[] toJSON(String networktype, String phoneId) { + String[] returnval = new String[rttsSmall.length]; + try { + for (int i = 0; i < rttsSmall.length; i++) { + JSONObject subtest = new JSONObject(); + subtest.put("rtt_low", rttsSmall[i]); + subtest.put("rtt_high", rttsLarge[i]); + subtest.put("lost_low", packetsLostSmall[i]); + subtest.put("lost_high", packetsLostLarge[i]); + subtest.put("signal_low", signalStrengthSmall[i]); + subtest.put("signal_high", signalStrengthLarge[i]); + subtest.put("error_low", errorCountSmall[i]); + subtest.put("error_high", errorCountLarge[i]); + subtest.put("network_type", networktype); + subtest.put("time_delay", i); + subtest.put("test_id", testId); + subtest.put("phone_id", phoneId); + returnval[i] = subtest.toString(); + Logger.w("Test ID for rrc inference test was " + this.testId); + } + } catch (JSONException e) { + Logger.e("Error converting RRC data to JSON"); + } + return returnval; + } + + /** + * Erase all data corresponding to a set of results with a particular interpacket interval + * + * @param index Index into the array of results. + */ + public void deleteItem(int index) { + rttsSmall[index] = -1; + rttsLarge[index] = -1; + packetsLostSmall[index] = -1; + packetsLostLarge[index] = -1; + signalStrengthSmall[index] = -1; + signalStrengthLarge[index] = -1; + errorCountSmall[index] = -1; + errorCountLarge[index] = -1; + } + + /** + * Save the data resulting from a given test. + * + * @param index Index into the array of results. + * @param rttMax Round trip time for large packets (generally 1KB) + * @param rttMin Round time time for small packets (generally empty) + * @param numPacketsLostMax Number of packets lost for the set of large packets, out of 10 + * @param numPacketsLostMin Likewise, for the small packets + * @param errorHigh Error count for the set of large packets + * @param errorLow Likewise, for the small packets + * @param signalHigh The signal strength when sending the large packets + * @param signalLow Likewise, for the small packets + */ + public void updateAll(int index, int rttMax, int rttMin, int numPacketsLostMax, + int numPacketsLostMin, int errorHigh, int errorLow, int signalHigh, int signalLow) { + this.rttsLarge[index] = (int) rttMax; + this.rttsSmall[index] = (int) rttMin; + this.packetsLostLarge[index] = numPacketsLostMax; + this.packetsLostSmall[index] = numPacketsLostMin; + this.errorCountLarge[index] = errorHigh; + this.errorCountSmall[index] = errorLow; + this.signalStrengthLarge[index] = signalHigh; + this.signalStrengthSmall[index] = signalLow; + } + + public void setRrcSizeTestData(int index, int size, long result, long testId) + throws MeasurementError { + packetSizes.add(new RrcSizeTestData(index, size, result, testId)); + } + + public String[] sizeDataToJSON(String networkType, String phoneId) { + String[] returnval = new String[packetSizes.size()]; + try { + for (int i = 0; i < packetSizes.size(); i++) { + returnval[i] = packetSizes.get(i).toJSON(networkType, phoneId).toString(); + } + } catch (JSONException e) { + Logger.e("Error converting RRC data to JSON"); + } + return returnval; + } + } + + /** + * Class for tracking if there has been interfering traffic. + * + * We need to know how many packets are expected. We can then use the global packet counters to + * see if more packets are sent than expected. + * + * @author sanae@umich.edu (Sanae Rosen) + * + */ + public static class PacketMonitor { + private long[] packetsFirst; + private long[] packetsLast; + boolean bySize = false; + + /** + * Initialize immediately before use. Values are time-sensitive. + */ + PacketMonitor() { + readCurrentPacketValues(); + } + + void setBySize() { + bySize = true; + } + + void readCurrentPacketValues() { + packetsFirst = getPacketsSent(); + } + + /** + * Call this to determine if packets have been sent since initializing. + * + * @param expectedRcv Number of packets expected to be received by the device since + * initialization + * @param expectedSent Number of packets expected to be sent by the device since initialization + * @return Whether or not there is interfering traffic + */ + boolean isTrafficInterfering(int expectedRcv, int expectedSent) { + packetsLast = getPacketsSent(); + + long rcvPackets = (packetsLast[0] - packetsFirst[0]); + long sentPackets = (packetsLast[1] - packetsFirst[1]); + if (rcvPackets <= expectedRcv && sentPackets <= expectedSent) { + Logger.d("No competing traffic, continue"); + return false; + } + Logger.d("Competing traffic, retry"); + + return true; + } + + /** + * Determine how many packets, so far, have been sent (the contents of /proc/net/dev/). This is + * a global value. We use this to determine if any other app anywhere on the phone may have sent + * interfering traffic that might have changed the RRC state without our knowledge. + * + * @return Two values: number of bytes or packets received at index 0, followed by the number + * sent at index 1. + */ + public long[] getPacketsSent() { + long[] retval = {-1, -1}; + if (bySize) { + retval[0] = TrafficStats.getMobileRxBytes(); + retval[1] = TrafficStats.getMobileTxBytes(); + + } else { + retval[0] = TrafficStats.getMobileRxPackets(); + retval[1] = TrafficStats.getMobileTxPackets(); + } + + return retval; + } + } + + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return RRCDesc.class; + } + + public RRCTask(MeasurementDesc desc) { + super(new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters)); + this.stopFlag = false; + this.duration = Config.DEFAULT_RRC_TASK_DURATION; + } + + protected RRCTask(Parcel in) { + super(in); + stopFlag = in.readByte() != 0; + duration = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public RRCTask createFromParcel(Parcel in) { + return new RRCTask(in); + } + + public RRCTask[] newArray(int size) { + return new RRCTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (stopFlag ? 1 : 0)); + dest.writeLong(duration); + } + + + + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + RRCDesc newDesc = + new RRCDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters); + return new RRCTask(newDesc); + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + try { + RRCDesc desc = runInferenceTests(); + MeasurementResult[] mrArray = new MeasurementResult[1]; + mrArray[0] = constructResultStandard(desc);; + return mrArray; + } catch(MeasurementError e) { + if (e.getMessage().equals("Rescheduled")) { + stopFlag=false; + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), RRCTask.TYPE, + System.currentTimeMillis() * 1000, TaskProgress.RESCHEDULED, this.measurementDesc); + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray= new MeasurementResult[1]; + mrArray[0]=result; + return mrArray; + } + else { + throw e; + } + } + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public String getType() { + return RRCTask.TYPE; + } + + @Override + public boolean stop() { + stopFlag = true; + return true; + } + + /** + * Helper function to construct MeasurementResults to submit to the server + * + * @param desc The data collected for the RRC test to be converted into something understandable + * by the server. + * @return A data structure that can be sent to the server. Contains only the results of the TCP, + * DNS and HTTP tests: the others are sent via a separate mechanism as they are stored + * separately. + */ + private MeasurementResult constructResultStandard(RRCDesc desc) { + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + TaskProgress taskProgress = TaskProgress.COMPLETED; + MeasurementResult result = + new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + RRCTask.TYPE, System.currentTimeMillis() * 1000, + taskProgress, this.measurementDesc); + + if (desc.runUpperLayerTests) { + result = desc.getResults(result); + } + + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + return result; + } + + /** + * The core RRC inference functionality is in this function. The steps involved can be summarized + * as follows: + *

    + *
  1. Fetch the last model generated by the server, if it exists.
  2. + *
  3. Check we are on a cellular network, otherwise abort.
  4. + *
  5. Inform all other tasks that they should delay network traffic until later.
  6. + *
  7. For every upper layer test, if upper layer tests and that test specifically are enabled, + * run that test.
  8. + *
+ * + * @return A data structure containing all the results and metadata surrounding the RRC inference + * tests performed. + * @throws MeasurementError + */ + private RRCDesc runInferenceTests() throws MeasurementError { + data_consumed = 0; + + // Fetch the existing model from the server, if it exists + RRCDesc desc = (RRCDesc) measurementDesc; + desc.testId = getTestId(PhoneUtils.getGlobalContext()); + Logger.w("Test ID set to " + desc.testId); + PhoneUtils utils = PhoneUtils.getPhoneUtils(); + desc.initializeExtraTaskResults(desc.times.length); + + // Check to make sure we are on a valid (i.e. cellular) network + if (utils.getNetwork() == "UNKNOWN" || utils.getNetwork() == "WIRELESS") { + Logger.d("Returning: network is" + utils.getNetwork() + " rssi " + utils.getCurrentRssi()); + return desc; + } + + RRCTestData data = new RRCTestData(desc.size, desc.testId); + try { + /* + * Suspend all other tasks performed by the app as they can interfere. Although we have a + * built-in check where we abort if traffic in the background interferes, in the past people + * have scheduled other tests to be every 5 minutes, which can cause the RRC task to never + * successfully complete without having to abort. + */ + // If the RRC task is enabled + if (desc.RRC) { + // Set up the connection to the echo server + Logger.d("Active inference: about to begin"); + Logger.d(desc.echoHost + ":" + desc.port); + InetAddress serverAddr = InetAddress.getByName(desc.echoHost); + + // Perform the RRC timer and latency inference task + Logger.d("Demotion inference: about to begin"); + desc = inferDemotion(serverAddr, desc, data, utils); + + Logger.d("About to save data"); + + try { + Logger.w("RRC: update the model on the GAE datastore"); + uploadRrcInferenceData(data); + Logger.d("Saving data complete"); + } catch (IOException e) { + e.printStackTrace(); + Logger.e("Data not saved: " + e.getMessage()); + } + } + + // Check if the upper layer tasks are enabled + if (desc.runUpperLayerTests) { + if (desc.DNS) { + Logger.w("Start DNS task"); + // Test the dependence of DNS latency on the RRC state, using + // the previously constructed model if available. + runDnsTest(desc.times, desc); + } + if (desc.TCP) { + Logger.w("Start TCP task"); + // Test the dependence of TCP latency on the RRC state. + runTCPHandshakeTest(desc.times, desc); + } + if (desc.HTTP) { + Logger.w("Start HTTP task"); + // Test the dependence of HTTP latency on the RRC state. + runHTTPTest(desc.times, desc); + } + + if (desc.SIZES) { + Logger.w("Start size dependence task"); + runSizeThresholdTest(desc.times, desc, data, desc.testId); + uploadRrcInferenceSizeData(data); + } + } + + + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (SocketException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (MeasurementError e) { + if (e.getMessage().equals("Rescheduled")) { + try { + // If the RRC task is enabled and we get partial data + if (desc.RRC && data.rttsSmall[0] != 7000) { + Logger.i("RRC Reschedule: update the model on the GAE datastore"); + uploadRrcInferenceData(data); + Logger.d("RRC Reschedule: Saving data complete"); + } + else { + Logger.i("RRC Reschedule: no model available"); + } + } catch (IOException ee) { + ee.printStackTrace(); + Logger.e("Data not saved: " + ee.getMessage()); + } + // Check if the upper layer tasks are enabled and we get partial results + if (desc.runUpperLayerTests && desc.SIZES && !data.packetSizes.isEmpty()) { + Logger.i("RRC Reschedule: update the size on the GAE datastore, array size " + data.packetSizes.size()); + uploadRrcInferenceSizeData(data); + Logger.d("RRC Reschedule: Saving data complete"); + } + else { + Logger.i("RRC Reschedule: no size information available"); + } + } + throw e; + } + + return desc; + } + + /** + * Impact of packet sizes on rrc inference results. + * + * @param sizeData Contains data to upload + */ + private void uploadRrcInferenceSizeData(RRCTestData sizeData) { + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + Checkin checkin = new Checkin(PhoneUtils.getGlobalContext()); + DeviceInfo info = phoneUtils.getDeviceInfo(); + String network_id = phoneUtils.getNetwork(); + String[] sizeParameters = sizeData.sizeDataToJSON(network_id, info.deviceId); + + try { + for (String parameter : sizeParameters) { + Logger.w("Uploading RRC size data: " + parameter); + String response = checkin.serviceRequest("rrc/uploadRRCInferenceSizes", parameter); + Logger.w("Response from GAE: " + response); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Send the RRC data to the server. + * + * Sent as a separate call because the data is formatted in a different, more complicated way than + * other measurement tasks. + * + * @param data Contains data to upload + * @throws IOException + */ + private void uploadRrcInferenceData(RRCTestData data) throws IOException { + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + Checkin checkin = new Checkin(PhoneUtils.getGlobalContext()); + DeviceInfo info = phoneUtils.getDeviceInfo(); + String network_id = phoneUtils.getNetwork(); + String[] parameters = data.toJSON(network_id, info.deviceId); + try { + for (String parameter : parameters) { + Logger.w("Uploading RRC raw data: " + parameter); + String response = checkin.serviceRequest("rrc/uploadRRCInference", parameter); + Logger.w("Response from GAE: " + response); + + Logger.i("TaskSchedule.uploadMeasurementResult() complete"); + } + } catch (IOException e) { + throw new IOException(e.getMessage()); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + + + /** + * Determines the packet size dependence of the RRC task. + * + * Given a specified list of inter-packet intervals, perform the RRC inference measurement for + * packets of sizes incremented by the size granularity specified. + * + * @param times Inter-packet intervals to test + * @param desc Parameters for the RRC inference task + * @param data Stores results of the RRC inference task + * @param testId A unique ID identifying this set of tests + */ + private void runSizeThresholdTest(final Integer[] times, RRCDesc desc, RRCTestData data, + long testId) throws MeasurementError { + + InetAddress serverAddr; + try { + serverAddr = InetAddress.getByName(desc.echoHost); + } catch (UnknownHostException e) { + Logger.e("Invalid or unreachable echo host. Test aborted."); + e.printStackTrace(); + return; + } + for (int i = 0; i < times.length; i++) { + for (int j = desc.sizeGranularity; j <= 1024; j += desc.sizeGranularity) { + // Sometimes the network can change in the middle of a test + try { + checkIfWifi(); + } catch (MeasurementError e) { + throw new MeasurementError("Rescheduled"); + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + try { + long result = inferDemotionPacketSize(serverAddr, times[i], desc, j); + data.setRrcSizeTestData(times[i], j, result, testId); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (MeasurementError e) { + e.printStackTrace(); + } + } + } + } + + /** + * Due to problems with how we detect interfering traffic, this test does not always work and + * results should be treated with caution. I am keeping this test around, though, as we can still + * get data on how much HTTP handshakes vary in how long they take. + * + * The "times" are the inter-packet intervals at which to run the test. Ideally these should be + * based on the model constructed by the server, a default assumed value is used in their absence. + * + * Based on the time it takes to load a response from the page. + * + * This test is not currently as accurate as the other tests, for reasons described below, and has + * thus been disabled. + * + * @param times List of inter-packet intervals, in half-second increments, at which to run the + * tests + * @param desc Stores parameters for the RRC inference tests in general + * @throws MeasurementError + */ + private void runHTTPTest(final Integer[] times, RRCDesc desc) throws MeasurementError { + /* + * Length of time it takes to request and read in a page. + */ + + Logger.d("Active inference HTTP test: about to begin"); + if (times.length != desc.httpTest.length) { + desc.httpTest = new int[times.length]; + } + long startTime = 0; + long endTime = 0; + try { + for (int i = 0; i < times.length; i++) { + // We try until we reach a threshhold or until there is no + // competing traffic. + for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { + // Sometimes the network can change in the middle of a test + try { + checkIfWifi(); + } catch (MeasurementError e) { + throw new MeasurementError("Rescheduled"); + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + /* + * We keep track of the packets sent at the beginning and end of the test so we can detect + * if there is competing traffic anywhere on the phone. + */ + PacketMonitor packetMonitor = new PacketMonitor(); + + // Initiate the desired RRC state by sending a large enough packet + // to go to DCH and waiting for the specified amount of time + try { + InetAddress serverAddr; + serverAddr = InetAddress.getByName(desc.echoHost); + sendPacket(serverAddr, desc.MAX, desc); + + waitTime(times[i] * desc.GRANULARITY, true); + + } catch (InterruptedException e1) { + e1.printStackTrace(); + continue; + } catch (UnknownHostException e) { + e.printStackTrace(); + continue; + } catch (IOException e) { + e.printStackTrace(); + continue; + } + + long currentRxTx=Util.getCurrentRxTxBytes(); + startTime = System.currentTimeMillis(); + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(); + + request.setURI(new URI("http://" + desc.target + "?dummy=" + i + "" + j)); + + HttpResponse response = client.execute(request); + endTime = System.currentTimeMillis(); + + BufferedReader in = null; + in = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + StringBuffer sb = new StringBuffer(""); + String line = ""; + + while ((line = in.readLine()) != null) { + sb.append(line + "\n"); + } + in.close(); + + //This may overestimate the data consumed, but there's no good way + // to tell what was us and what was another app + incrementData(Util.getCurrentRxTxBytes()-currentRxTx); + + + // not really accurate, just rules out the worst cases of interference + if (!packetMonitor.isTrafficInterfering(100, 100)) { + break; + } + startTime = 0; + endTime = 0; + + } + + long rtt = endTime - startTime; + try { + desc.setHttp(i, (int) rtt); + } catch (MeasurementError e) { + e.printStackTrace(); + } + Logger.d("Time for Http" + rtt); + } + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + /** + * The "times" are the inter-packet intervals at which to run the test. Ideally these should be + * based on the model constructed by the server, a default assumed value is used in their absence. + * + *
    + *
  1. vSend a packet to initiate the RRC state desired.
  2. + *
  3. Create a randomly generated host name (to ensure that the host name is not cached). I found + * on some devices that even when you clear the cache manually, the data remains in the cache.
  4. + *
  5. Time how long it took to look it up.
  6. + *
  7. Count the total packets sent, globally on the phone. If more packets were sent than
  8. + * expected, abort and try again. + *
  9. Otherwise, save the data for that test and move to the next inter-packet interval.
  10. + *
+ * + * Test is similar to the approach taken in DnsLookUpTask.java. + * + * @param times List of inter-packet intervals, in half-second increments, at which to run the + * test + * @param desc Stores parameters for the RRC inference tests in general + * @throws MeasurementError + */ + + public void runDnsTest(final Integer[] times, RRCDesc desc) throws MeasurementError { + Logger.d("Active inference DNS test: about to begin"); + if (times.length != desc.dnsTest.length) { + desc.dnsTest = new int[times.length]; + } + long dataConsumedThisTask = 0; + + long startTime = 0; + long endTime = 0; + + // For each inter-packet interval... + for (int i = 0; i < times.length; i++) { + // On a failure, try again until a threshold is reached. + for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { + + // Sometimes the network can change in the middle of a test + try { + checkIfWifi(); + } catch (MeasurementError e) { + throw new MeasurementError("Rescheduled"); + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + /* + * We keep track of the packets sent at the beginning and end of the test so we can detect + * if there is competing traffic anywhere on the phone. + */ + + PacketMonitor packetMonitor = new PacketMonitor(); + + + // Initiate the desired RRC state by sending a large enough packet + // to go to DCH and waiting for the specified amount of time + try { + InetAddress serverAddr; + serverAddr = InetAddress.getByName(desc.echoHost); + sendPacket(serverAddr, desc.MAX, desc); + waitTime(times[i] * desc.GRANULARITY, true); + } catch (InterruptedException e1) { + e1.printStackTrace(); + continue; + } catch (UnknownHostException e) { + e.printStackTrace(); + continue; + } catch (IOException e) { + e.printStackTrace(); + continue; + } + + // Create a random URL, to avoid the caching problem + UUID uuid = UUID.randomUUID(); + String host = uuid.toString() + ".com"; + // Start measuring the time to complete the task + startTime = System.currentTimeMillis(); + try { + @SuppressWarnings("unused") + InetAddress serverAddr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + // we do this on purpose! Since it's a fake URL the lookup will fail + } + // When we fail to find the URL, we stop timing + endTime = System.currentTimeMillis(); + + dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE; + + // Check how many packets were sent again. If the expected number + // of packets were sent, we can finish and go to the next task. + // Otherwise, we have to try again. + if (!packetMonitor.isTrafficInterfering(5, 5)) { + break; + } + + startTime = 0; + endTime = 0; + } + + // If we broke out of the try-again loop, the last set of results are + // valid and we can save them. + long rtt = endTime - startTime; + try { + desc.setDns(i, (int) rtt); + } catch (MeasurementError e) { + e.printStackTrace(); + } + Logger.d("Time for DNS" + rtt); + } + incrementData(dataConsumedThisTask); + } + + /** + * Time how long it takes to do a TCP 3-way handshake, starting from the induced RRC state. + * + *
    + *
  1. Send a packet to initiate the RRC state desired.
  2. + *
  3. Open a TCP connection to the echo host server.
  4. + *
  5. Time how long it took to look it up.
  6. + *
  7. Count the total packets sent, globally on the phone. If more packets were sent than + * expected, abort and try again.
  8. + *
  9. Otherwise, save the data for that test and move to the next inter-packet interval. + *
+ * + * @param times List of inter-packet intervals, in half-second increments, at which to run the + * test + * @param desc Stores parameters for the RRC inference tests in general + * @throws MeasurementError + */ + public void runTCPHandshakeTest(final Integer[] times, RRCDesc desc) throws MeasurementError { + Logger.d("Active inference TCP test: about to begin"); + if (times.length != desc.tcpTest.length) { + desc.tcpTest = new int[times.length]; + } + long startTime = 0; + long endTime = 0; + long dataConsumedThisTask = 0; + + try { + // For each inter-packet interval... + for (int i = 0; i < times.length; i++) { + // On a failure, try again until a threshhold is reached. + for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { + try { + checkIfWifi(); + } catch (MeasurementError e) { + throw new MeasurementError("Rescheduled"); + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + PacketMonitor packetMonitor = new PacketMonitor(); + + // Induce DCH then wait for specified time + InetAddress serverAddr; + serverAddr = InetAddress.getByName(desc.echoHost); + sendPacket(serverAddr, desc.MAX, desc); + waitTime(times[i] * 500, true); + + // begin test. We test the time to do a 3-way handshake only. + startTime = System.currentTimeMillis(); + + serverAddr = InetAddress.getByName(desc.target); + // three-way handshake done when socket created + Socket socket = new Socket(serverAddr, 80); + endTime = System.currentTimeMillis(); + + // Not exact, but also a smallish task... + dataConsumedThisTask += DnsLookupTask.AVG_DATA_USAGE_BYTE; + + + // Check how many packets were sent again. If the expected number + // of packets were sent, we can finish and go to the next task. + // Otherwise, we have to try again. + if (!packetMonitor.isTrafficInterfering(5, 4)) { + socket.close(); + break; + } + startTime = 0; + endTime = 0; + socket.close(); + } + long rtt = endTime - startTime; + try { + desc.setTcp(i, (int) rtt); + } catch (MeasurementError e) { + e.printStackTrace(); + } + Logger.d("Time for TCP" + rtt); + } + } catch (InterruptedException e1) { + e1.printStackTrace(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + incrementData(dataConsumedThisTask); + } + + /** + * + * For all time intervals specified, go through and perform the RRC inference test. + * + * The way this test works, at a high level, is: + *
    + *
  1. Send a large packet to induce DCH (or the equivalent)
  2. + *
  3. Wait for an amount of time, t
  4. + *
  5. Send a large packet and measure the round-trip time
  6. + *
  7. Wait for another amount of time, t
  8. + *
  9. Send a small packet and measure the round-trip time 6. Repeat for all specified values of + * t. These start at 0 and increase by GRANULARITY, "size" times.
  10. + *
+ * + * The size of "small" and "large" packets are defined in the parameters. We observe the total + * packets sent to make sure there is no interfering traffic. + * + * Packets are UDP packets. + * + * From this, we can infer the timers associated with RRC states. By sending a large packet, we + * induce the highest power state. Waiting a number of seconds afterwards allows us to demote to + * the next state. Sending a packet and observing the RTT allows us to infer if a state promotion + * had to take place. + * + * FACH is characterized by different state promotion times for large and small packets. + * + * @param serverAddr Address of the echo server + * @param desc Stores the parameters for the RRC tests + * @param data Stores the results of the RRC tests + * @param utils For fetching the signal strength when the test is performed + * @return The parameters for the RRC tests + * @throws InterruptedException + * @throws IOException + * @throws MeasurementError + */ + private RRCDesc inferDemotion(InetAddress serverAddr, RRCDesc desc, RRCTestData data, + PhoneUtils utils) throws InterruptedException, IOException, MeasurementError { + Logger.d("Demotion basic test"); + + for (int i = 0; i <= desc.size; i++) { + + try { + checkIfWifi(); + } catch (MeasurementError e) { + throw new MeasurementError("Rescheduled"); + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + inferDemotionHelper(serverAddr, i, data, desc, utils); + Logger.d("Finished demotion test with length" + i); + + // Note that we scale from 0-90 to save some stuff for upper layer tests. + // If we wanted to really do this properly we could scale + // according to how long each task should take. + } + return desc; + } + + @Override + public String toString() { + RRCDesc desc = (RRCDesc) measurementDesc; + return "[RRC]\n Echo Server: " + desc.echoHost + "\n Target: " + desc.target + + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + desc.startTime; + } + + /********************************************************************* + * UTILITIES * + *********************************************************************/ + + /** + * + * Sleep for the amount of time indicated. + * + * @param timeToSleep Time for which we pause the current thread + * @param useMs Toggles between units of milliseconds for the first parameter (true) and + * seconds(false). + * @throws InterruptedException + */ + public static void waitTime(int timeToSleep, boolean useMs) throws InterruptedException { + + if (!useMs) { + Logger.d("Wait for n s: " + timeToSleep); + timeToSleep = timeToSleep * 1000; + } + else { + Logger.d("Wait for n ms: " + timeToSleep); + } + Thread.sleep(timeToSleep); + } + + /** + * Sends a bunch of UDP packets of the size indicated and wait for the response. + * + * Counts how long it takes for all the packets to return. PAckets are currently not labelled: the + * total time is the time for the first packet to leave until the last packet arrives. AFter 7000 + * ms it is assumed packets are lost and the socket times out. In that case, the number of packets + * lost is recorded. + * + * @param serverAddr server to which to send the packets + * @param size size of the packets + * @param num number of packets to send + * @param packetSize size of the packets sent + * @param port port to send the packets to + * @return first value: the amount of time to send all packets and get a response. second value: + * number of packets lost, on a timeout. + * @throws IOException + */ + public static long[] sendMultiPackets(InetAddress serverAddr, int size, int num, int packetSize, + int port) throws IOException { + + long startTime = 0; + long endTime = 0; + byte[] buf = new byte[size]; + byte[] rcvBuf = new byte[packetSize]; + long[] retval = {-1, -1}; + long numLost = 0; + int i = 0; + long dataConsumedThisTask = 0; + + DatagramSocket socket = new DatagramSocket(); + DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length); + DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port); + + // number * (packet sent + packet received) + dataConsumedThisTask += num * (size + packetSize); + + try { + socket.setSoTimeout(7000); + startTime = System.currentTimeMillis(); + Logger.d("Sending packet, waiting for response "); + for (i = 0; i < num; i++) { + socket.send(packet); + } + for (i = 0; i < num; i++) { + socket.receive(packetRcv); + if (i == 0) { + + endTime = System.currentTimeMillis(); + } + } + } catch (SocketTimeoutException e) { + Logger.d("Timed out"); + numLost += (num - i); + socket.close(); + } + Logger.d("Sending complete: " + endTime); + + retval[0] = endTime - startTime; + retval[1] = numLost; + + incrementData(dataConsumedThisTask); + return retval; + } + + /** + * Helper function that sends a single packet and receives an empty packet back. + * + * @param serverAddr Echo server to calculate round trip + * @param size size of packet to send in bytes + * @param desc Holds parameters for the RRC inference task + * @return The round trip time for the packet + * @throws IOException + */ + private static long sendPacket(InetAddress serverAddr, int size, RRCDesc desc) throws IOException { + return sendPacket(serverAddr, size, desc.MIN, desc.port); + } + + /** + * Send a single packet of the size indicated and wait for a response. + * + * After 7000 ms, time out and return a value of -1 (meaning no response). Otherwise, return the + * time from when the packet was sent to when a response was returned by the echo server. + * + * @param serverAddr Echo server to calculate round trip + * @param size size of packet to send in bytes + * @param rcvSize size of packets sent from the echo server + * @param port where the echo server is listening + * @return The round trip time for the packet + * @throws IOException + */ + public static long sendPacket(InetAddress serverAddr, int size, int rcvSize, int port) + throws IOException { + long startTime = 0; + byte[] buf = new byte[size]; + byte[] rcvBuf = new byte[rcvSize]; + long dataConsumedThisTask = 0; + + DatagramSocket socket = new DatagramSocket(); + DatagramPacket packetRcv = new DatagramPacket(rcvBuf, rcvBuf.length); + + dataConsumedThisTask += (size + rcvSize); + + DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port); + + try { + socket.setSoTimeout(7000); + startTime = System.currentTimeMillis(); + Logger.d("Sending packet, waiting for response "); + + socket.send(packet); + socket.receive(packetRcv); + } catch (SocketTimeoutException e) { + Logger.d("Timed out, trying again"); + socket.close(); + return -1; + } + long endTime = System.currentTimeMillis(); + Logger.d("Sending complete: " + endTime); + incrementData(dataConsumedThisTask); + return endTime - startTime; + } + + /** + * Performs a single RRC inference test to account for packet sizes. + * + * Sends a packet, waits for the specified length of time, then sends a cluster of packets of the + * specified size. + * + * @param serverAddr Echo server to calculate round trip + * @param wait Time to wait between packets, in milliseconds. + * @param desc Holds parameters for the RRC inference task + * @param size Size, in bytes, of the packet to send. + * @return The amount of time to send all packets and get a response. + * @throws IOException + * @throws InterruptedException + */ + public static long inferDemotionPacketSize(InetAddress serverAddr, int wait, RRCDesc desc, + int size) throws IOException, InterruptedException { + long retval = -1; + for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { + Logger.d("Active inference: determine packet size, size " + size + " interval " + wait); + + // Induce the highest power state + sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port); + + // WAit for the specified amount of time + waitTime(wait * desc.GRANULARITY, true); + + // Send the specified packet size + long[] rtts = sendMultiPackets(serverAddr, size, 1, desc.MIN, desc.port); + long rttPacket = rtts[0]; + + PacketMonitor packetMonitor = new PacketMonitor(); + if (!packetMonitor.isTrafficInterfering(3, 3)) { + retval = rttPacket; + break; + } + } + return retval; + } + + private long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data, + RRCDesc desc, PhoneUtils utils) throws IOException, InterruptedException { + return inferDemotionHelper(serverAddr, wait, data, desc, wait, utils); + } + + /** + * One component of the RRC inference task. + * + *
    + *
  1. Induce the highest-power RRC state by sending a large packet.
  2. + *
  3. Wait the indicated number of seconds.
  4. + *
  5. Send a series of 10 large packets at once. Measure: a) Time for all packets to be echoed + * back b) number of packets lost, if any c) associated signal strength d) error rate is currently + * not implemented.
  6. + *
  7. Check if the expected number of packets were sent while performing a test. If too many + * packets were sent, abort.
  8. + *
+ * + * @param serverAddr Echo server to calculate round trip + * @param wait Time in milliseconds to pause between packets sent + * @param data Stores the results of the RRC inference tests + * @param desc Stores parameters for the RRC inference tests + * @param index Index of the current test, corresponds to the inter-packet time in intervals of + * half milliseconds + * @param utils Used to retrieve the phone's RSSI at the time of collecting the data + * @return first value: the amount of time to send all small packets and get a response. Second + * value: time to send all large packets and get a response. + * @throws IOException + * @throws InterruptedException + */ + public static long[] inferDemotionHelper(InetAddress serverAddr, int wait, RRCTestData data, + RRCDesc desc, int index, PhoneUtils utils) throws IOException, InterruptedException { + /** + * Once we generalize the RRC state inference problem, this is what we will use (since in + * general, RRC state can differ between large and small packets). Gives the RTT for a large + * packet and a small packet for a given time after inducing DCH state. Granularity currently + * half-seconds but can easily be increased. + * + * Measures packets sent before and after to make sure no extra packets were sent, and retries + * on a failure. Also checks that there was no timeout and retries on a failure. + */ + long rttLargePacket = -1; + long rttSmallPacket = -1; + int packetsLostSmall = 0; + int packetsLostLarge = 0; + + int errorCountLarge = 0; + int errorCountSmall = 0; + int signalStrengthLarge = 0; + int signalStrengthSmall = 0; + + for (int j = 0; j < desc.GIVEUP_THRESHHOLD; j++) { + Logger.d("Active inference: about to begin helper"); + + PacketMonitor packetMonitor = new PacketMonitor(); + + // Induce the highest power state + sendPacket(serverAddr, desc.MAX, desc.MIN, desc.port); + + // WAit for the specified amount of time + waitTime(wait * desc.GRANULARITY, true); + + // Send a bunch of large packets, all at once, and take measurements on the result +// signalStrengthLarge = utils.getCurrentRssi(); + long[] retval = sendMultiPackets(serverAddr, desc.MAX, 10, desc.MIN, desc.port); + packetsLostSmall = (int) retval[1]; + rttLargePacket = retval[0]; + + // wait for the specified amount of time + waitTime(wait * desc.GRANULARITY, true); + + // Send a bunch of small packets, all at once, and take measurements on the result +// signalStrengthSmall = utils.getCurrentRssi(); + retval = sendMultiPackets(serverAddr, desc.MIN, 10, desc.MIN, desc.port); + packetsLostLarge = (int) retval[1]; + rttSmallPacket = retval[0]; + + if (!packetMonitor.isTrafficInterfering(21, 21)) { + break; + } + Logger.d("Try again."); + rttLargePacket = -1; + rttSmallPacket = -1; + packetsLostSmall = 0; + packetsLostLarge = 0; + + errorCountLarge = 0; + errorCountSmall = 0; + signalStrengthLarge = 0; + signalStrengthSmall = 0; + } + + Logger.d("3G demotion, lower bound: rtts are:" + rttLargePacket + " " + rttSmallPacket + " " + + packetsLostSmall + " " + packetsLostLarge); + + long[] retval = {rttLargePacket, rttSmallPacket}; + data.updateAll(index, (int) rttLargePacket, (int) rttSmallPacket, packetsLostSmall, + packetsLostLarge, errorCountLarge, errorCountSmall, signalStrengthLarge, + signalStrengthSmall); + + return retval; + } + + /** + * Keep a global counter that labels each test with a unique, increasing integer. + * + * @param context Any context instance, needed to fetch the last test id from permanent storage + * @return The unique (for this device) test ID generated. + */ + public static synchronized int getTestId(Context context) { + SharedPreferences prefs = context.getSharedPreferences("test_ids", Context.MODE_PRIVATE); + int testid = prefs.getInt("test_id", 0) + 1; + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt("test_id", testid); + editor.commit(); + return testid; + } + + /** + * If on wifi, suspend the task until we go back to the cellular network. Use exponential back-off + * to calculate the wait time, with a limit of 500 s. Once the limit is reached, unpause other + * tasks. Repause traffic if we are good to resume again. + * + * @throws MeasurementError + * + */ + public void checkIfWifi() throws MeasurementError { + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + + int timeToWait = 10; + while (true) { + if (phoneUtils.getNetwork() != PhoneUtils.NETWORK_WIFI) { + // RRCTrafficControl.PauseTraffic(); + return; + } + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + // if (timeToWait < 60) { + // RRCTrafficControl.UnPauseTraffic(); + // } + Logger.d("RRCTask: on Wifi, try again later: " + phoneUtils.getNetwork()); + if (timeToWait < 500) { // 500s, or a bit over 8 minutes. + timeToWait = timeToWait * 2; + } else { + // if it's taking a while, give up this RRC test + Logger.e("Still not on cellular, timeout after backoff to 500s"); + throw new MeasurementError("Still not on cellular, timeout after backoff to 500s"); + } + try { + waitTime(timeToWait, false); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public long getDuration() { + return this.duration; + } + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + private synchronized static void incrementData(long data_increment) { + data_consumed += data_increment; +} + +/** + * For RRC inference, we calculate this precisely based on the number and size + * of packets sent. For the TCP handshake and DNS tasks, we use small, fixed + * values based on the average data consumption measured for those tasks. + * + * For the HTTP task, we count all data sent during the task time towards + * the budget. This will tend to overestimate the data used, but due to + * retransmissions, etc, it is impossible to get a remotely accurate estimate + * otherwise, I found. + */ + @Override + public long getDataConsumed() { + return data_consumed; + } +} diff --git a/src/com/mobilyzer/measurements/SequentialTask.java b/src/com/mobilyzer/measurements/SequentialTask.java new file mode 100644 index 0000000..61a4cb8 --- /dev/null +++ b/src/com/mobilyzer/measurements/SequentialTask.java @@ -0,0 +1,259 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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 java.io.InvalidClassException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * + * @author Ashkan Nikravesh (ashnik@umich.edu) + * + * Sequential Task is a measurement task that can execute more than one measurement task + * in series. + */ +public class SequentialTask extends MeasurementTask{ + private List tasks; + + private ExecutorService executor; + + // Type name for internal use + public static final String TYPE = "sequential"; + // Human readable name for the task + public static final String DESCRIPTOR = "sequential"; + private volatile boolean stopFlag; + private long duration; + private volatile MeasurementTask currentTask; + + public static class SequentialDesc extends MeasurementDesc { + + public SequentialDesc(String key, Date startTime, + Date endTime, double intervalSec, long count, long priority, + int contextIntervalSec, Map params) + throws InvalidParameterException { + super(SequentialTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + // initializeParams(params); + + } + + @Override + protected void initializeParams(Map params) { + } + + @Override + public String getType() { + return SequentialTask.TYPE; + } + + protected SequentialDesc(Parcel in) { + super(in); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SequentialDesc createFromParcel(Parcel in) { + return new SequentialDesc(in); + } + + public SequentialDesc[] newArray(int size) { + return new SequentialDesc[size]; + } + }; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return SequentialDesc.class; + } + + + + public SequentialTask(MeasurementDesc desc, ArrayList tasks) { + super(new SequentialDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, + desc.parameters)); + this.tasks=(List) tasks.clone(); + //we are using executor because it has builtin support for execution timeouts. + executor=Executors.newSingleThreadExecutor(); + long totalduration=0; + for(MeasurementTask mt: tasks){ + totalduration+=mt.getDuration(); + } + this.duration=totalduration; + this.stopFlag=false; + this.currentTask=null; + } + + protected SequentialTask(Parcel in) { + super(in); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + // we cannot directly cast Parcelable[] to MeasurementTask[]. Cast them one-by-one + Parcelable[] tempTasks = in.readParcelableArray(loader); + executor=Executors.newSingleThreadExecutor(); + tasks = new ArrayList(); + long totalduration=0; + for ( Parcelable pTask : tempTasks ) { + MeasurementTask mt = (MeasurementTask) pTask; + tasks.add(mt); + totalduration+=mt.getDuration(); + } + this.duration = totalduration; + this.stopFlag=false; + this.currentTask=null; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SequentialTask createFromParcel(Parcel in) { + return new SequentialTask(in); + } + + public SequentialTask[] newArray(int size) { + return new SequentialTask[size]; + } + }; + + @Override + public int describeContents() { + return super.describeContents(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelableArray(tasks.toArray(new MeasurementTask[tasks.size()]), flags); + } + + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + + ArrayList allResults=new ArrayList(); + try { + // futures=executor.invokeAll(this.tasks,timeout,TimeUnit.MILLISECONDS); + for(MeasurementTask mt: tasks){ + mt.setContext(this.getContext()); + if(stopFlag){ + throw new MeasurementError("Cancelled"); + } + Future f=executor.submit(mt); + currentTask=mt; + MeasurementResult[] results; + //specifying timeout for each task based on its duration + try { + results = f.get( mt.getDuration()==0 ? + Config.DEFAULT_TASK_DURATION_TIMEOUT * 2 : mt.getDuration() * 2, + TimeUnit.MILLISECONDS); + for(int i=0;i newTaskList=new ArrayList(); + for(MeasurementTask mt: tasks){ + newTaskList.add(mt.clone()); + } + return new SequentialTask(newDesc,newTaskList); + } + + @Override + public boolean stop() { + if(currentTask.stop()){ + stopFlag=true; + executor.shutdown(); + return true; + }else{ + return false; + } + + } + + @Override + public long getDuration() { + return duration; + } + + @Override + public void setDuration(long newDuration) { + if(newDuration<0){ + this.duration=0; + }else{ + this.duration=newDuration; + } + } + + public MeasurementTask[] getTasks() { + return tasks.toArray(new MeasurementTask[tasks.size()]); + } + + //TODO + @Override + public long getDataConsumed() { + return 0; + } + +} diff --git a/src/com/mobilyzer/measurements/TCPThroughputTask.java b/src/com/mobilyzer/measurements/TCPThroughputTask.java new file mode 100644 index 0000000..8c294a8 --- /dev/null +++ b/src/com/mobilyzer/measurements/TCPThroughputTask.java @@ -0,0 +1,838 @@ +//Copyright 2012 RobustNet Lab, University of Michigan. All Rights Reserved. + +package com.mobilyzer.measurements; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.Random; + +import android.content.Context; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementScheduler; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MLabNS; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; + + +/** + * @author Haokun Luo + * + * TCP Throughput is a measurement task for cellular network throughput. + * 1. Uplink: the mobile device continuously sends packets with consistent + * packet size. We send packets for a fixed amount of time, and we sample + * each throughput value at a smaller period. We use the median of all the + * sampling result as the final measurement result. The result is calculated + * at the server side, and send back to the device. + * 2. Downlink: similar methodology as uplink. Only difference is that the + * device is receiving packets from the server, and calculate the result + * locally. + */ +public class TCPThroughputTask extends MeasurementTask { + // default constant here + public static final String DESCRIPTOR = "TCP Speed Test"; + public static final int PORT_DOWNLINK = 6001; + public static final int PORT_UPLINK = 6002; + public static final int PORT_CONFIG = 6003; + public static final String TYPE = "tcpthroughput"; + + // Timing related + public final int BUFFER_SIZE = 5000; + public static final long DURATION_IN_SEC = 15; + public final int KSEC = 1000; + public static final long SAMPLE_PERIOD_IN_SEC = 1; + public static final long SLOW_START_PERIOD_IN_SEC = 5; + public static final int TCP_TIMEOUT_IN_SEC = 30; + // largest non-fragment packet size in LTE (uplink) + public static final int THROUGHPUT_UP_PKT_SIZE_MAX = 1357; + public static final int THROUGHPUT_UP_PKT_SIZE_MIN = 700; + + // Data related + private final int KBYTE = 1024; + private static final int DATA_LIMIT_MB_UP = 5; + private static final int DATA_LIMIT_MB_DOWN = 10; + private boolean DATA_LIMIT_ON = true; + private boolean DATA_LIMIT_EXCEEDED = false; + private static final String UPLINK_FINISH_MSG = "*"; + + private Context context = null; + + // helper variables + private int accumulativeSize = 0; + private Random randStr = new Random(); + private ArrayList samplingResults = new ArrayList(); + //start time of each sampling period + private long startSampleTime = 0; + private String serverVersion = ""; + private long taskStartTime = 0; + private double taskDuration = 0; + //uplink accumulative data + private int totalSendSize = 0; + // downlink accumulative data + private int totalRevSize = 0; + + private long duration; + private TaskProgress taskProgress; + private volatile boolean stopFlag; + + private Context IM_context = null; // added by Clarence + private TaskProgress Intermediate_TaskProgress = TaskProgress.COMPLETED; //added by Clarence + + //added by Clarence, add broadcast to send the intermediate results + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, Context context) { + this.IM_context = context; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.IM_context.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } + + // class constructor + public TCPThroughputTask(MeasurementDesc desc) { + super(new TCPThroughputDesc(desc.key, desc.startTime, desc.endTime, + desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, + desc.parameters)); + this.taskProgress=TaskProgress.FAILED; + this.stopFlag=false; + this.duration=(long)(this.KSEC* + ((TCPThroughputDesc)measurementDesc).duration_period_sec + + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); + Logger.i("Create new throughput task"); + } + + + protected TCPThroughputTask(Parcel in) { + super(in); + taskProgress = (TaskProgress)in.readSerializable(); + stopFlag = in.readByte() != 0; + duration = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public TCPThroughputTask createFromParcel(Parcel in) { + return new TCPThroughputTask(in); + } + + public TCPThroughputTask[] newArray(int size) { + return new TCPThroughputTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeSerializable(taskProgress); + dest.writeByte((byte) (stopFlag ? 1 : 0)); + dest.writeLong(duration); + } + /** + * There are seven parameters specifically for this experiment: + * 1. data_limit_mb_up: uplink cellular network data limit + * 2. data_limit_mb_down: downlink cellular network data limit + * 3. duration_period_sec : downlink maximum experiment duration period + * 4. pkt_size_up_bytes: the size each packet in the uplink + * 5. sample_period_sec : the small interval to calculate current throughput result + * 6. slow_start_period_sec : waiting period to avoid TCP slow start + * 7. tcp_timeout_sec: TCP connection timeout + */ + + public static class TCPThroughputDesc extends MeasurementDesc { + // declared parameters + public double data_limit_mb_up = + TCPThroughputTask.DATA_LIMIT_MB_UP; + public double data_limit_mb_down = + TCPThroughputTask.DATA_LIMIT_MB_DOWN; + public boolean dir_up = false; + public double duration_period_sec = + TCPThroughputTask.DURATION_IN_SEC; + public int pkt_size_up_bytes = + TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX; + public double sample_period_sec = + TCPThroughputTask.SAMPLE_PERIOD_IN_SEC; + public double slow_start_period_sec = + TCPThroughputTask.SLOW_START_PERIOD_IN_SEC; + public String target = null; + public double tcp_timeout_sec = TCPThroughputTask.TCP_TIMEOUT_IN_SEC; + + public TCPThroughputDesc(String key, Date startTime, + Date endTime, double intervalSec, long count, + long priority, int contextIntervalSec, Map params) + throws InvalidParameterException { + super(TCPThroughputTask.TYPE, key, startTime, endTime, intervalSec, count, + priority, contextIntervalSec, params); + initializeParams(params); + if (this.target == null || this.target.length() == 0) { + throw new InvalidParameterException("TCPThroughputTask null target"); + } + } + + protected TCPThroughputDesc(Parcel in) { + super(in); + data_limit_mb_up = in.readDouble(); + data_limit_mb_down = in.readDouble(); + dir_up = in.readByte() != 0; + duration_period_sec = in.readDouble(); + pkt_size_up_bytes = in.readInt(); + sample_period_sec = in.readDouble(); + slow_start_period_sec = in.readDouble(); + target = in.readString(); + tcp_timeout_sec = in.readDouble(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public TCPThroughputDesc createFromParcel(Parcel in) { + return new TCPThroughputDesc(in); + } + + public TCPThroughputDesc[] newArray(int size) { + return new TCPThroughputDesc[size]; + } + }; + + @Override + public int describeContents() { + return super.describeContents(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeDouble(data_limit_mb_up); + dest.writeDouble(data_limit_mb_down); + dest.writeByte((byte) (dir_up ? 1 : 0)); + dest.writeDouble(duration_period_sec); + dest.writeInt(pkt_size_up_bytes); + dest.writeDouble(sample_period_sec); + dest.writeDouble(slow_start_period_sec); + dest.writeString(target); + dest.writeDouble(tcp_timeout_sec); + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + if ( (target = params.get("target")) == null ) { + target = MLabNS.TARGET; + } + + try { + String readVal = null; + if ((readVal = params.get("data_limit_mb_down")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.data_limit_mb_down = Double.parseDouble(readVal); + if (this.data_limit_mb_down > TCPThroughputTask.DATA_LIMIT_MB_DOWN) { + this.data_limit_mb_down = TCPThroughputTask.DATA_LIMIT_MB_DOWN; + } + } + + if ((readVal = params.get("data_limit_mb_up")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.data_limit_mb_up = Double.parseDouble(readVal); + if (this.data_limit_mb_up > TCPThroughputTask.DATA_LIMIT_MB_UP) { + this.data_limit_mb_up = TCPThroughputTask.DATA_LIMIT_MB_UP; + } + } + if ((readVal = params.get("duration_period_sec")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.duration_period_sec = Double.parseDouble(readVal); + if (this.duration_period_sec > TCPThroughputTask.DURATION_IN_SEC) { + this.duration_period_sec = TCPThroughputTask.DURATION_IN_SEC; + } + } + if ((readVal = params.get("pkt_size_up_bytes")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.pkt_size_up_bytes = Integer.parseInt(readVal); + if (this.pkt_size_up_bytes > TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX) { + this.pkt_size_up_bytes = TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MAX; + } + if (this.pkt_size_up_bytes < TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MIN) { + this.pkt_size_up_bytes = TCPThroughputTask.THROUGHPUT_UP_PKT_SIZE_MIN; + } + } + if ((readVal = params.get("sample_period_sec")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.sample_period_sec = Double.parseDouble(readVal); + if (this.sample_period_sec > TCPThroughputTask.DURATION_IN_SEC/2) { + this.sample_period_sec = TCPThroughputTask.DURATION_IN_SEC/2; + } + } + if ((readVal = params.get("slow_start_period_sec")) != null + && readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.slow_start_period_sec = Double.parseDouble(readVal); + if (this.slow_start_period_sec > TCPThroughputTask.DURATION_IN_SEC/2) { + this.slow_start_period_sec = TCPThroughputTask.DURATION_IN_SEC/2; + } + } + if ((readVal = params.get("tcp_timeout_sec")) != null && + readVal.length() > 0 && Integer.parseInt(readVal) > 0) { + this.tcp_timeout_sec = Integer.parseInt(readVal)*1000; + if (this.tcp_timeout_sec > TCPThroughputTask.TCP_TIMEOUT_IN_SEC) { + this.tcp_timeout_sec = TCPThroughputTask.TCP_TIMEOUT_IN_SEC; + } + } + } catch (NumberFormatException e) { + throw new InvalidParameterException("TCP Throughput Task invalid parameters."); + } + + String dir = null; + if ((dir = params.get("dir_up")) != null && dir.length() > 0) { + if (dir.compareTo("Up") == 0 || dir.compareTo("true") == 0) { + this.dir_up = true; + } + } + } + + @Override + public String getType() { + return TCPThroughputTask.TYPE; + } + + /** + * Find the median value from a TCPThroughput JSON result string (already sorted) + * Suppose N is the number of results. If N is odd, we pick the result with index + * (N-1)/2. If N is even, we take the mean value between index N/2 and N/2-1 + * + * @return -1 fail to create result + * @return median value result + */ + public double calMedianSpeedFromTCPThroughputOutput(String outputInJSON) { + if (outputInJSON == null || + outputInJSON.equals("") || + outputInJSON.equals("[]") || + outputInJSON.charAt(0) != '[' || + outputInJSON.charAt(outputInJSON.length()-1) != ']') { + return -1; + } + + String[] splitResult = outputInJSON.substring(1, + outputInJSON.length()-1).split(","); + int resultLen = splitResult.length; + if (resultLen <= 0) + return 0.0; + double result = 0.0; + if (resultLen % 2 == 0) { + result = (Double.parseDouble(splitResult[resultLen / 2]) + + Double.parseDouble(splitResult[resultLen / 2 - 1])) / 2; + } else { + result = Double.parseDouble(splitResult[(resultLen - 1) / 2]); + } + return result; + } + } + + /** + * Make a deep cloning of the task + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + TCPThroughputDesc newDesc = new TCPThroughputDesc( + desc.key, desc.startTime, + desc.endTime, desc.intervalSec, desc.count, desc.priority, + desc.contextIntervalSec, desc.parameters); + return new TCPThroughputTask(newDesc); + } + + @Override + public String getType() { + return TCPThroughputTask.TYPE; + } + + @Override + public String getDescriptor() { + return TCPThroughputTask.DESCRIPTOR; + } + + /** + * This will be printed to the device log console. Make sure it's well + * structured and human readable + */ + @Override + public String toString() { + TCPThroughputDesc desc = (TCPThroughputDesc) measurementDesc; + String resp; + + if (desc.dir_up) { + resp = "[TCP Uplink]\n"; + } else { + resp = "[TCP Downlink]\n"; + } + + resp += " Target: " + desc.target + "\n Interval (sec): " + + desc.intervalSec + "\n Next run: " + desc.startTime; + + return resp; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return TCPThroughputDesc.class; + } + + + @Override + public MeasurementResult[] call() throws MeasurementError { + this.taskProgress=TaskProgress.FAILED; + TCPThroughputDesc desc = (TCPThroughputDesc)measurementDesc; + + // Apply MLabNS lookup to fetch FQDN + if (!desc.target.equals(MLabNS.TARGET)) { + Logger.i("Not using MLab server!"); + throw new InvalidParameterException("Unknown target " + desc.target + + " for TCPThroughput"); + } + + try { + ArrayList mlabResult = MLabNS.Lookup(context, "mobiperf"); + if (mlabResult.size() == 1) { + desc.target = mlabResult.get(0); + } else { + throw new MeasurementError("Invalid MLabNS result"); + } + } catch (InvalidParameterException e) { + throw new MeasurementError(e.getMessage()); + } + Logger.i("Setting target to: " + desc.target); + + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + + // reset the data limit if the phone is under Wifi + if (phoneUtils.getNetwork().equals(phoneUtils.NETWORK_WIFI)) { + Logger.i("Detect Wifi network"); + this.DATA_LIMIT_ON = false; + } + + Logger.i("Running TCPThroughput on " + desc.target); + try { + // fetch server information + if (!acquireServerConfig()) { + throw new MeasurementError("Fail to acquire server configuration"); + } + Logger.i("Server version is " + this.serverVersion); + if (desc.dir_up == true) { + uplink(); + if(stopFlag){ + throw new MeasurementError("Cancelled"); + } + Logger.i("Uplink measurement result is:"); + } + else { + this.taskStartTime = System.currentTimeMillis(); + downlink(); + if(stopFlag){ + throw new MeasurementError("Cancelled"); + } + Logger.i("Downlink measurement result is:"); + } + this.taskProgress=TaskProgress.COMPLETED; + } catch (MeasurementError e) { + throw e; + } catch (IOException e) { + Logger.e("Error close the socket for " + desc.type); + throw new MeasurementError("Error close the socket for " + desc.type); + } catch (InterruptedException e) { + Logger.e("Interrupted captured"); + throw new MeasurementError("Task gets interrrupted"); + } + + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), TCPThroughputTask.TYPE, + System.currentTimeMillis() * 1000, taskProgress, + this.measurementDesc); + // TODO (Haokun): add more results if necessary + result.addResult("tcp_speed_results", this.samplingResults); + result.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); + result.addResult("duration", this.taskDuration); + result.addResult("server_version", this.serverVersion); + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + MeasurementResult[] mrArray= new MeasurementResult[1]; + mrArray[0]=result; + return mrArray; + } + + /***************************************************************** + * Core measurement functions definitions + ***************************************************************** + * acquire server configuration information + * 1) m-lab slice version + * + * @return: true -- successful acquire data from M-Lab slice + * @return: false -- failure to acquire data from M-Lab slice + */ + private boolean acquireServerConfig() throws MeasurementError, IOException, + InterruptedException { + Socket tcpSocket = null; + InputStream iStream = null; + boolean result = false; + try { + tcpSocket = new Socket(); + buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, + TCPThroughputTask.PORT_CONFIG); + iStream = tcpSocket.getInputStream(); + } catch (IOException e) { + throw new MeasurementError("Error open uplink socket at " + + ((TCPThroughputDesc)measurementDesc).target + + " with port " + + TCPThroughputTask.PORT_CONFIG); + } + + try { + // read from server side configuration + byte [] resultMsg = new byte[this.BUFFER_SIZE]; + int resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length); + if (resultMsgLen > 0) { + // TODO (Haokun): Maybe switch to JSON for multiple acquired data + // currently use one double number + this.serverVersion = new String(resultMsg).substring(0, resultMsgLen); + result = true; + } + } catch (IOException e) { + throw new MeasurementError("Error to acquire configuration from " + + ((TCPThroughputDesc)measurementDesc).target); + } finally { + iStream.close(); + tcpSocket.close(); + Logger.i("Close server Config socket"); + } + return result; + } + + /* Uplink measurement task + * @throws IOException + * @throws InterruptedException + */ + private void uplink() throws MeasurementError, IOException, InterruptedException { + Logger.i("Start uplink task on " + ((TCPThroughputDesc)measurementDesc).target); + Socket tcpSocket = null; + InputStream iStream = null; + OutputStream oStream = null; + + + + try { + tcpSocket = new Socket(); + buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, + TCPThroughputTask.PORT_UPLINK); + oStream = tcpSocket.getOutputStream(); + iStream = tcpSocket.getInputStream(); + } catch (IOException e){ + e.printStackTrace(); + throw new MeasurementError("Error open uplink socket at " + + ((TCPThroughputDesc)measurementDesc).target + + " with port " + + TCPThroughputTask.PORT_UPLINK); + } + + long startTime = System.currentTimeMillis(); + long endTime = startTime; + + + int data_limit_byte_up = + (int)(((TCPThroughputDesc)measurementDesc).data_limit_mb_up + *this.KBYTE*this.KBYTE); + byte[] uplinkBuffer = + new byte[((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes]; + this.genRandomByteArray(uplinkBuffer); + + + try { + + + long totalDuration = (long)(this.KSEC* + ((TCPThroughputDesc)measurementDesc).duration_period_sec + + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); + do { + + if(stopFlag){ + throw new MeasurementError("Cancelled"); + } + + oStream.write(uplinkBuffer, 0, uplinkBuffer.length); + oStream.flush(); + endTime = System.currentTimeMillis(); + + + this.totalSendSize += ((TCPThroughputDesc)measurementDesc).pkt_size_up_bytes; + if (this.DATA_LIMIT_ON && + this.totalSendSize >= data_limit_byte_up) { + Logger.i("Detect uplink exceeding limitation " + + (double)((TCPThroughputDesc)measurementDesc).data_limit_mb_up + " MB"); + this.DATA_LIMIT_EXCEEDED = true; + break; + } + + // propagate every quarter + + } while ((endTime - startTime) < totalDuration); + + // convert into seconds + this.taskDuration = (double)(endTime - startTime) / 1000.0; + Logger.i("Uplink total data comsumption is " + + (double)this.totalSendSize/(1024*1024) + " MB"); + // send last message with special content + uplinkBuffer = TCPThroughputTask.UPLINK_FINISH_MSG.getBytes(); + oStream.write(uplinkBuffer, 0, uplinkBuffer.length); + oStream.flush(); + // read from server side results + byte [] resultMsg = new byte[this.BUFFER_SIZE]; + int resultMsgLen = iStream.read(resultMsg, 0, resultMsg.length); + if (resultMsgLen > 0) { + + MeasurementResult IntermediateResult = null; //added by Clarence + + String resultMsgStr = new String(resultMsg).substring(0, resultMsgLen); + // Sample result string is "1111.11#2222.22#3333.33"; + Logger.i("Uplink result from server is " + resultMsgStr); + String [] tps_result_str = resultMsgStr.split("#"); + double sampleResult; + for (int i = 0; i < tps_result_str.length; i++) { + sampleResult = Double.valueOf(tps_result_str[i]); + this.samplingResults = this.insertWithOrder(this.samplingResults, + sampleResult); + + //added by Clarence + this.IM_context = this.getContext(); + if (this.IM_context != null){ + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TCPThroughputTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + IntermediateResult.addResult("tcp_speed_results", this.samplingResults); + IntermediateResult.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); + IntermediateResult.addResult("duration", this.taskDuration); + IntermediateResult.addResult("server_version", this.serverVersion); + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.IM_context); + + } + } + } + Logger.i("Total number of sampling result is " + this.samplingResults.size()); + + } catch (OutOfMemoryError e) { + throw new MeasurementError("Detect out of memory during Uplink task."); + } catch (IOException e) { + throw new MeasurementError("Error to send/receive data to " + + ((TCPThroughputDesc)measurementDesc).target); + } finally { + iStream.close(); + oStream.close(); + tcpSocket.close(); + Logger.i("Close uplink socket"); + } + } + + /** + * Downlink measurement task + */ + private void downlink() throws MeasurementError, IOException { + Logger.i("Start downlink task on " + + ((TCPThroughputDesc)measurementDesc).target); + Socket tcpSocket = null; + InputStream iStream = null; + try { + tcpSocket = new Socket(); + buildUpSocket(tcpSocket, ((TCPThroughputDesc)measurementDesc).target, + TCPThroughputTask.PORT_DOWNLINK); + iStream = tcpSocket.getInputStream(); + } catch (IOException i) { + Logger.e("Downlink socket opening error" + i.getCause().toString()); + throw new MeasurementError("Error to open downlink socket at " + + ((TCPThroughputDesc)measurementDesc).target + + " with port " + + TCPThroughputTask.PORT_DOWNLINK); + } + try { + int read_bytes = 0; + + int data_limit_byte_down = (int)(this.KBYTE*this.KBYTE* + ((TCPThroughputDesc)measurementDesc).data_limit_mb_down); + byte[] buffer = new byte[this.BUFFER_SIZE]; + long totalDuration = (long)(this.KSEC* + ((TCPThroughputDesc)measurementDesc).duration_period_sec + + ((TCPThroughputDesc)measurementDesc).slow_start_period_sec); + do { + + if(stopFlag){ + throw new MeasurementError("Cancelled"); + } + + read_bytes = iStream.read(buffer, 0, buffer.length); + updateSize(read_bytes); + + this.totalRevSize += read_bytes; + if (this.DATA_LIMIT_ON && + this.totalRevSize >= data_limit_byte_down) { + Logger.i("Detect downlink data limitation exceed with " + + ((TCPThroughputDesc)measurementDesc).data_limit_mb_down + " MB"); + this.DATA_LIMIT_EXCEEDED = true; + break; + } + + + } while (read_bytes >= 0); + + // convert milliseconds to seconds + this.taskDuration = (System.currentTimeMillis() - + (double) this.taskStartTime) / 1000.0; + Logger.i("Total download data is " + + (double)this.totalRevSize/(1024*1024) + " MB"); + Logger.i("Total number of sampling result is " + + this.samplingResults.size()); + + } catch (OutOfMemoryError e) { + throw new MeasurementError("Detect out of memory at Downlink task."); + } catch (IOException e) { + throw new MeasurementError("Error to receive data from " + + ((TCPThroughputDesc)measurementDesc).target); + } finally { + iStream.close(); + tcpSocket.close(); + Logger.i("Close downlink socket"); + } + } + + /***************************************************************** + * Helper functions + ***************************************************************** + * update the total received packet size + * @param time period increment + */ + private void updateSize(int delta) { + + MeasurementResult IntermediateResult = null; //added by Clarence + + double gtime = System.currentTimeMillis() - this.taskStartTime; + //ignore slow start + if (gtime<((TCPThroughputDesc)measurementDesc).slow_start_period_sec*this.KSEC) + return; + if (this.startSampleTime == 0) { + this.startSampleTime = System.currentTimeMillis(); + this.accumulativeSize = 0; + } + this.accumulativeSize += delta; + double time = System.currentTimeMillis() - this.startSampleTime; + if (time < ((TCPThroughputDesc)measurementDesc).sample_period_sec*this.KSEC) { + return; + } else { + double throughput = (double)this.accumulativeSize * 8.0 / time; + this.samplingResults = this.insertWithOrder(this.samplingResults, throughput); + this.accumulativeSize = 0; + this.startSampleTime = System.currentTimeMillis(); + + //added by Clarence + this.IM_context = this.getContext(); + if (this.IM_context != null){ + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TCPThroughputTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + IntermediateResult.addResult("tcp_speed_results", this.samplingResults); + IntermediateResult.addResult("data_limit_exceeded", this.DATA_LIMIT_EXCEEDED); + IntermediateResult.addResult("duration", time); + IntermediateResult.addResult("server_version", this.serverVersion); + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.IM_context); + + } + + } + } + + private void buildUpSocket(Socket tcpSocket, String hostname, int portNum) + throws IOException { + TCPThroughputDesc desc = (TCPThroughputDesc) measurementDesc; + SocketAddress remoteAddr = new InetSocketAddress(hostname, portNum); + tcpSocket.connect(remoteAddr, (int)desc.tcp_timeout_sec*this.KSEC); + tcpSocket.setSoTimeout((int)desc.tcp_timeout_sec*this.KSEC); + tcpSocket.setTcpNoDelay(true); + } + + private void genRandomByteArray(byte[] byteArray) { + for (int i = 0; i < byteArray.length; i++) { + byteArray[i] = (byte)('a' + randStr.nextInt(26)); + } + } + + // insert element with ascending order, i.e. insertion sort + private ArrayList insertWithOrder(ArrayList array, double item) { + int i; + for (i = 0; i < array.size(); i++ ) { + if (item < array.get(i)) { + break; + } + } + array.add(i,item); + return array; + } + + @Override + public long getDuration() { + return this.duration; + } + + @Override + public void setDuration(long newDuration) { + if(newDuration<0){ + this.duration=0; + }else{ + this.duration=newDuration; + } + } + + @Override + public boolean stop() { + stopFlag=true; + return true; + } + + /** + * Based on the measured total data sent and received, the same returned as + * a measurement result + */ + @Override + public long getDataConsumed() { + return totalSendSize + totalRevSize; + } +} diff --git a/src/com/mobilyzer/measurements/TracerouteTask.java b/src/com/mobilyzer/measurements/TracerouteTask.java new file mode 100755 index 0000000..10ef4ba --- /dev/null +++ b/src/com/mobilyzer/measurements/TracerouteTask.java @@ -0,0 +1,853 @@ +/* + * 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 java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InvalidClassException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import android.content.Context; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mobilyzer.Config; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementScheduler; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.PreemptibleMeasurementTask; +import com.mobilyzer.UpdateIntent; +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; + + + +/** + * A Callable task that handles Traceroute measurements + */ +public class TracerouteTask extends MeasurementTask implements PreemptibleMeasurementTask { + // Type name for internal use + public static final String TYPE = "traceroute"; + // Human readable name for the task + public static final String DESCRIPTOR = "traceroute"; + /* + * Default payload size of the ICMP packet, plus the 8-byte ICMP header resulting in a total of + * 64-byte ICMP packet + */ + public static final int DEFAULT_PING_PACKET_SIZE = 56; + public static final int DEFAULT_PING_TIMEOUT = 10; + public static final int DEFAULT_MAX_HOP_CNT = 30; + public static final int DEFAULT_PARALLEL_PROBE_NUM = 1; + // Used to compute progress for user + public static final int EXPECTED_HOP_CNT = 20; + public static final int DEFAULT_PINGS_PER_HOP = 3; + + private long duration; + public ArrayList resultsArray; + private TaskProgress taskProgress; + + + private volatile boolean stopFlag; + private volatile boolean pauseFlag; + public ArrayList hopHosts;// TODO: change it to private + private long totalRunningTime; + private int ttl; + private int maxHopCount; + + + // Track data consumption for this task to avoid exceeding user's limit + private long dataConsumed; + private Context context = null; // added by Clarence + private TaskProgress Intermediate_TaskProgress = TaskProgress.COMPLETED; //added by Clarence + + //added by Clarence, add broadcast to send the intermediate results + + private void broadcastIntermediateMeasurement(MeasurementResult[] results, Context context) { + this.context = context; + Intent intent = new Intent(); + intent.setAction(UpdateIntent.MEASUREMENT_INTERMEDIATE_PROGRESS_UPDATE_ACTION); + //TODO fixed one value priority for all users task? + intent.putExtra(UpdateIntent.TASK_PRIORITY_PAYLOAD, + MeasurementTask.USER_PRIORITY); + intent.putExtra(UpdateIntent.TASKID_PAYLOAD, this.getTaskId()); + intent.putExtra(UpdateIntent.CLIENTKEY_PAYLOAD, this.getKey()); + + if (results != null){ + + //intent.putExtra(UpdateIntent.TASK_STATUS_PAYLOAD, Config.TASK_FINISHED); + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, results); + + this.context.sendBroadcast(intent); + }else{ + intent.putExtra(UpdateIntent.INTERMEDIATE_RESULT_PAYLOAD, "No intermediate results are broadcasted"); + + } + + } + + /** + * The description of the Traceroute measurement + */ + public static class TracerouteDesc extends MeasurementDesc { + // the host name or IP address to use as the target of the traceroute. + public String target; + // the packet per ICMP ping in the unit of bytes + private int packetSizeByte; + // the number of seconds we wait for a ping response. + private int pingTimeoutSec; + // the interval between successive pings in seconds + private double pingIntervalSec; + // the number of pings we use for each ttl value + private int pingsPerHop; + // the total number of pings will send before we declarethe traceroute fails + private int maxHopCount; + // the location of the ping binary. Only used internally + private String pingExe; + // TODO, this should be moved into MeasurementDesc if we want to have that for all measurement + // types + public String preCondition; + + private int parallelProbeNum; + + + + public TracerouteDesc(String key, Date startTime, Date endTime, double intervalSec, long count, + long priority, int contextIntervalSec, Map params) + throws InvalidParameterException { + super(TracerouteTask.TYPE, key, startTime, endTime, intervalSec, count, priority, + contextIntervalSec, params); + initializeParams(params); + + if (target == null || target.length() == 0) { + throw new InvalidParameterException("Target of traceroute cannot be null"); + } + } + + @Override + public String getType() { + return TracerouteTask.TYPE; + } + + @Override + protected void initializeParams(Map params) { + + if (params == null) { + return; + } + + // HTTP specific parameters according to the design document + this.target = params.get("target"); + try { + String val; + if ((val = params.get("packet_size_byte")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.packetSizeByte = Integer.parseInt(val); + } else { + this.packetSizeByte = TracerouteTask.DEFAULT_PING_PACKET_SIZE; + } + if ((val = params.get("ping_timeout_sec")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.pingTimeoutSec = Integer.parseInt(val); + } else { + this.pingTimeoutSec = TracerouteTask.DEFAULT_PING_TIMEOUT; + } + if ((val = params.get("ping_interval_sec")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.pingIntervalSec = Integer.parseInt(val); + } else { + this.pingIntervalSec = Config.DEFAULT_INTERVAL_BETWEEN_ICMP_PACKET_SEC; + } + if ((val = params.get("pings_per_hop")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.pingsPerHop = Integer.parseInt(val); + } else { + this.pingsPerHop = TracerouteTask.DEFAULT_PINGS_PER_HOP; + } + if ((val = params.get("max_hop_count")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.maxHopCount = Integer.parseInt(val); + } else { + this.maxHopCount = TracerouteTask.DEFAULT_MAX_HOP_CNT; + } + if ((val = params.get("precond")) != null && val.length() > 0) { + this.preCondition = params.get("precond"); + } else { + this.preCondition = null; + } + if ((val = params.get("parallel_probe_num")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.parallelProbeNum = Integer.parseInt(val); + } else { + this.parallelProbeNum = TracerouteTask.DEFAULT_PARALLEL_PROBE_NUM; + } + } catch (NumberFormatException e) { + throw new InvalidParameterException("PingTask cannot be created due " + "to invalid params"); + } + + + + } + + protected TracerouteDesc(Parcel in) { + super(in); + target = in.readString(); + packetSizeByte = in.readInt(); + pingTimeoutSec = in.readInt(); + pingIntervalSec = in.readDouble(); + pingsPerHop = in.readInt(); + maxHopCount = in.readInt(); + pingExe = in.readString(); + preCondition = in.readString(); + parallelProbeNum = in.readInt(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public TracerouteDesc createFromParcel(Parcel in) { + return new TracerouteDesc(in); + } + + public TracerouteDesc[] newArray(int size) { + return new TracerouteDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(target); + dest.writeInt(packetSizeByte); + dest.writeInt(pingTimeoutSec); + dest.writeDouble(pingIntervalSec); + dest.writeInt(pingsPerHop); + dest.writeInt(maxHopCount); + dest.writeString(pingExe); + dest.writeString(preCondition); + dest.writeInt(parallelProbeNum); + } + } + + public TracerouteTask(MeasurementDesc desc) { + super(new TracerouteDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters)); + this.duration = Config.TRACEROUTE_TASK_DURATION; + this.taskProgress = TaskProgress.FAILED; + this.stopFlag = false; + this.pauseFlag = false; + this.hopHosts = new ArrayList(); + this.ttl = 1; + this.maxHopCount = ((TracerouteDesc) this.measurementDesc).maxHopCount; + this.totalRunningTime = 0; + this.dataConsumed = 0; + } + + protected TracerouteTask(Parcel in) { + super(in); + duration = in.readLong(); + taskProgress = (TaskProgress) in.readSerializable(); + stopFlag = (in.readByte() != 0); + pauseFlag = (in.readByte() != 0); + hopHosts = new ArrayList(); + ttl = in.readInt(); + maxHopCount = ((TracerouteDesc) this.measurementDesc).maxHopCount; + totalRunningTime = in.readLong(); + dataConsumed = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public TracerouteTask createFromParcel(Parcel in) { + return new TracerouteTask(in); + } + + public TracerouteTask[] newArray(int size) { + return new TracerouteTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(duration); + dest.writeSerializable(taskProgress); + dest.writeByte((byte) (stopFlag ? 1 : 0)); + dest.writeByte((byte) (pauseFlag ? 1 : 0)); + dest.writeInt(ttl); + dest.writeLong(totalRunningTime); + dest.writeLong(dataConsumed); + } + + /** + * Returns a copy of the TracerouteTask + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + TracerouteDesc newDesc = + new TracerouteDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters); + return new TracerouteTask(newDesc); + } + + @Override + public MeasurementResult[] call() throws MeasurementError { + + TracerouteDesc task = (TracerouteDesc) this.measurementDesc; + // int maxHopCount = task.maxHopCount; + // int ttl = 1; + String hostIp = null; + String target = task.target; + taskProgress = TaskProgress.FAILED; + stopFlag = false; + pauseFlag = false; + + + + Logger.d("Starting traceroute on host " + task.target); + + try { + InetAddress hostInetAddr = InetAddress.getByName(target); + hostIp = hostInetAddr.getHostAddress(); + // add support for ipv6 + int ipByteLen = hostInetAddr.getAddress().length; + Logger.i("IP address length is " + ipByteLen); + Logger.i("IP is " + hostIp); + task.pingExe = Util.pingExecutableBasedOnIPType(ipByteLen); + Logger.i("Ping executable is " + task.pingExe); + if (task.pingExe == null) { + Logger.e("Ping Executable not found"); + throw new MeasurementError("Ping Executable not found"); + } + } catch (UnknownHostException e) { + Logger.e("Cannont resolve host " + target); + throw new MeasurementError("target " + target + " cannot be resolved"); + } + MeasurementResult result = null; + MeasurementResult IntermediateResult = null; //added by Clarence + + + + + ExecutorService hopExecutorService = Executors.newFixedThreadPool(task.parallelProbeNum); + CompletionService taskCompletionService = + new ExecutorCompletionService(hopExecutorService); + + /* + * Current traceroute implementation sends out three ICMP probes per TTL. One ping every 0.2s is + * the lower bound before some platforms requires root to run ping. We ping once every time to + * get a rough rtt as we cannot get the exact rtt from the output of the ping command with ttl + * being set + */ + boolean[] hopsStatus = new boolean[maxHopCount]; + for (int i = maxHopCount; i > 0; i--) { + hopsStatus[i - 1] = false; + String command = + Util.constructCommand(task.pingExe, "-n", "-t", ttl, "-s", task.packetSizeByte, "-c 1", + target); + taskCompletionService.submit(new HopExecutor(task.pingsPerHop, command, hostIp, ttl)); + ttl++; + } + + for (int tasksHandled = 0; tasksHandled < maxHopCount; tasksHandled++) { + + + try { + Future hopResult = taskCompletionService.take(); + HopInfo hop = hopResult.get(); + hopHosts.add(hop); + HashSet hostsAtThisDistance = hop.hosts; + hopsStatus[hop.ttl - 1] = true; + for (String ip : hostsAtThisDistance) { + // If we have reached the final destination hostIp, + // print it out and clean up + boolean allHopsAreDone = true; + if (ip.compareTo(hostIp) == 0) { + for (int i = 0; i < hop.ttl; i++) { + if (!hopsStatus[hop.ttl - 1]) { + allHopsAreDone=false; + } + } + + if(allHopsAreDone){ + + hopExecutorService.shutdownNow(); + Logger.i(hop.ttl + ": " + hostIp); + Logger.i(" Finished! " + target + " reached in " + hop.ttl + " hops"); + taskProgress = TaskProgress.COMPLETED; + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + result = + new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), TracerouteTask.TYPE, + System.currentTimeMillis() * 1000, taskProgress, this.measurementDesc); + + result.addResult("num_hops", hop.ttl); + + for (int i = 0; i < hopHosts.size(); i++) { + HopInfo hopInfo = hopHosts.get(i); + int hostIdx = 1; + for (String host : hopInfo.hosts) { + result.addResult("hop_" + hopInfo.ttl + "_addr_" + hostIdx++, host); + } + result.addResult("hop_" + hopInfo.ttl + "_rtt_ms", String.format("%.3f", hopInfo.rtt)); + if (hopInfo.hosts.contains(hostIp)){ + break; + } + } + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + + MeasurementResult[] mrArray = new MeasurementResult[1]; + mrArray[0] = result; + return mrArray; + + } + + } + } + //added by Clarence + this.context = this.getContext(); + if (this.context != null){ + + PhoneUtils Intermediate_phoneUtils = PhoneUtils.getPhoneUtils(); + IntermediateResult = new MeasurementResult(Intermediate_phoneUtils.getDeviceInfo().deviceId, + Intermediate_phoneUtils.getDeviceProperty(this.getKey()),TracerouteTask.TYPE, + System.currentTimeMillis()*1000,Intermediate_TaskProgress,this.measurementDesc); + + IntermediateResult.addResult("num_hops", hop.ttl); + + for (int i = 0; i < hopHosts.size(); i++) { + HopInfo IM_hopInfo = hopHosts.get(i); + int IM_hostIdx = 1; //added by Clarence + for (String IM_host : IM_hopInfo.hosts) { + IntermediateResult.addResult("hop_" + IM_hopInfo.ttl + "_addr_" + IM_hostIdx++, IM_host); + } + IntermediateResult.addResult("hop_" + IM_hopInfo.ttl + "_rtt_ms", String.format("%.3f", IM_hopInfo.rtt)); + + } + +// for (String IM_host :hop.hosts){ +// IntermediateResult.addResult("hop_" + hop.ttl + "_addr_" + IM_hostIdx++, IM_host); +// } +// IntermediateResult.addResult("hop_" + hop.ttl + "_rtt_ms", String.format("%.3f", hop.rtt)); + + MeasurementResult[] IM_mrArray = new MeasurementResult[1]; + IM_mrArray[0] = IntermediateResult; + broadcastIntermediateMeasurement(IM_mrArray,this.context); + } + + + + + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + + + } + + + + + Logger.e("cannot perform traceroute to " + task.target); + throw new MeasurementError("cannot perform traceroute to " + task.target); + } + + + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return TracerouteDesc.class; + } + + @Override + public String getType() { + return TracerouteTask.TYPE; + } + + @Override + public String getDescriptor() { + return DESCRIPTOR; + } + + private void cleanUp(Process proc) { + if (proc != null) { + // destroy() closes all open streams + proc.destroy(); + } + } + + private HashSet processPingOutput(BufferedReader br, String hostIp) throws IOException { + HashSet hostsAtThisDistance = new HashSet(); + String line = null; + while ((line = br.readLine()) != null) { + if (line.startsWith("From")) { + String ip = getHostIp(line); + if (ip != null && ip.compareTo(hostIp) != 0) { + Logger.d("IP: " + ip); + hostsAtThisDistance.add(ip); + } + } else if (line.contains("time=")) { + hostsAtThisDistance.add(hostIp); + } + } + return hostsAtThisDistance; + } + + /* + * TODO(Wenjie): The current search for valid IPs assumes the IP string is not a proper substring + * of the space-separated tokens. For more robust searching in case different outputs from ping + * due to its different versions, we need to refine the search by testing weather any substring of + * the tokens contains a valid IP + */ + private String getHostIp(String line) { + String[] tokens = line.split(" "); + // In most cases, the second element in the array is the IP + String tempIp = tokens[1]; + /** + * In Android 4.3 or above, the second token of the result is like "192.168.1.1:". So we should + * remove the last ":" + */ + if (tempIp.endsWith(":")) { + tempIp = tempIp.substring(0, tempIp.length() - 1); + } + if (isValidIpv4Addr(tempIp) || isValidIpv6Addr(tempIp)) { + return tempIp; + } else { + for (int i = 0; i < tokens.length; i++) { + if (i == 1) { + // Examined already + continue; + } else { + if (isValidIpv4Addr(tokens[i]) || isValidIpv6Addr(tokens[i])) { + return tokens[i]; + } + } + } + } + + return null; + } + + // Tells whether the string is an valid IPv4 address + private boolean isValidIpv4Addr(String ip) { + String[] tokens = ip.split("\\."); + if (tokens.length == 4) { + for (int i = 0; i < 4; i++) { + try { + int val = Integer.parseInt(tokens[i]); + if (val < 0 || val > 255) { + return false; + } + } catch (NumberFormatException e) { + Logger.d(ip + " is not a valid IPv4 address"); + return false; + } + } + return true; + } + return false; + } + + // Tells whether the string is an valid IPv6 address + private boolean isValidIpv6Addr(String ip) { + int max = Integer.valueOf("FFFF", 16); + String[] tokens = ip.split("\\:"); + if (tokens.length <= 8) { + for (int i = 0; i < tokens.length; i++) { + try { + // zeros might get grouped + if (tokens[i].isEmpty()) + continue; + int val = Integer.parseInt(tokens[i], 16); + if (val < 0 || val > max) { + return false; + } + } catch (NumberFormatException e) { + Logger.d(ip + " is not a valid IPv6 address"); + return false; + } + } + return true; + } + return false; + } + + private class HopInfo { + // The hosts at a given hop distance + public HashSet hosts; + // The average RRT for this hop distance + public double rtt; + public int ttl; + + protected HopInfo(HashSet hosts, double rtt, int ttl) { + this.hosts = hosts; + this.rtt = rtt; + this.ttl = ttl; + } + } + + @Override + public String toString() { + TracerouteDesc desc = (TracerouteDesc) measurementDesc; + return "[Traceroute]\n Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec + + "\n Next run: " + desc.startTime; + } + + + + // Measure the actual ping process execution time + private class ProcWrapper extends Thread { + public long duration = 0; + private final Process process; + private Integer exitStatus = null; + + private ProcWrapper(Process process) { + this.process = process; + } + + public void run() { + try { + long startTime = System.currentTimeMillis(); + exitStatus = process.waitFor(); + duration = System.currentTimeMillis() - startTime; + } catch (InterruptedException e) { + Logger.e("Traceroute thread gets interrupted"); + } + } + } + + + class HopExecutor implements Callable { + private int pingsPerHop; + private String command; + private String hostIp; + private Process pingProc = null; + private int ttl; + + public HopExecutor(int pingsPerHop, String command, String hostIp, int ttl) { + this.pingsPerHop = pingsPerHop; + this.command = command; + this.ttl = ttl; + this.hostIp = hostIp; + } + + @Override + public HopInfo call() { + double rtt = 0; + HashSet hostsAtThisDistance = new HashSet(); + try { + + int effectiveTask = 0; + ExecutorService executor = Executors.newFixedThreadPool(pingsPerHop); + ArrayList workers = new ArrayList(); + for (int i = 0; i < pingsPerHop; i++) { + // Actual packet is 28 bytes larger than the size specified. + // Three packets are sent in each direction + // dataConsumed += (task.packetSizeByte + 28) * 2 * 3;//TODO + + pingProc = Runtime.getRuntime().exec(command); + + Runnable worker = new PingExecutor(pingProc, hostIp); + executor.execute(worker); + workers.add(worker); + } + + executor.shutdown(); + try { + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + for (Runnable w : workers) { + rtt += ((PingExecutor) w).getRtt(); + if (((PingExecutor) w).getRtt() != 0) { + effectiveTask++; + for (String h : ((PingExecutor) w).getHosts()) { + hostsAtThisDistance.add(h); + } + } + } + + rtt = (effectiveTask != 0) ? (rtt / effectiveTask) : -1; + if (rtt == -1) { + String Unreachablehost = ""; + for (int i = 0; i < pingsPerHop; i++) { + Unreachablehost += "* "; + } + hostsAtThisDistance.add(Unreachablehost); + } + + + } catch (SecurityException e) { + Logger.e("Does not have the permission to run ping on this device"); + } catch (IOException e) { + Logger.e("The ping program cannot be executed"); + Logger.e(e.getMessage()); + } finally { + cleanUp(pingProc); + } + + return new HopInfo(hostsAtThisDistance, rtt, ttl); + + } + + } + + class PingExecutor implements Runnable { + private Process proc; + private double rtt; + private String hostIp; + private HashSet hosts; + + public PingExecutor(Process proc, String hostIp) { + this.proc = proc; + rtt = 0; + this.hostIp = hostIp; + hosts = new HashSet(); + } + + public double getRtt() { + return rtt; + } + + public HashSet getHosts() { + return hosts; + } + + + @Override + public void run() { + // Wait for process to finish + // Enforce thread timeout if pingProc doesn't respond + ProcWrapper procwrapper = new ProcWrapper(proc); + procwrapper.start(); + try { + long pingThreadTimeout = 5000; + procwrapper.join(pingThreadTimeout); + if (procwrapper.exitStatus == null) + throw new TimeoutException(); + } catch (InterruptedException ex) { + procwrapper.interrupt(); + Thread.currentThread().interrupt(); + Logger.e("Traceroute process gets interrupted"); + cleanUp(proc); + return; + } catch (TimeoutException e) { + Logger.e("Traceroute process timeout"); + cleanUp(proc); + return; + } + rtt += procwrapper.duration; + + + // Grab the output of the process that runs the ping command + InputStream is = proc.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + /* + * Process each line of the ping output and extracts the intermediate hops into + * hostAtThisDistance + */ + try { + hosts = processPingOutput(br, hostIp); + } catch (IOException e) { + + e.printStackTrace(); + } + cleanUp(proc); + // try { + // Thread.sleep((long) (task.pingIntervalSec * 1000)); + // } catch (InterruptedException e) { + // Logger.i("Sleep interrupted between ping intervals"); + // } + + } + + + } + + @Override + public long getDuration() { + return this.duration - this.totalRunningTime; + } + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + + @Override + public boolean pause() { + pauseFlag = true; + return true; + } + + @Override + public boolean stop() { + stopFlag = true; + // cleanUp(pingProc);TODO + return true; + } + + @Override + public long getTotalRunningTime() { + return this.totalRunningTime; + } + + @Override + public void updateTotalRunningTime(long duration) { + this.totalRunningTime += duration; + } + + /** + * Based on counting the number of pings sent + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } + + + +} diff --git a/src/com/mobilyzer/measurements/UDPBurstTask.java b/src/com/mobilyzer/measurements/UDPBurstTask.java new file mode 100644 index 0000000..2b85429 --- /dev/null +++ b/src/com/mobilyzer/measurements/UDPBurstTask.java @@ -0,0 +1,855 @@ +/* + * Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.*; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; + +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.MLabNS; +import com.mobilyzer.util.PhoneUtils; + +/** + * + * UDPBurstTask provides two types of measurements, Burst Up and Burst Down, described next. + * + * 1. UDPBurst Up: the device sends a burst of UDPBurstCount UDP packets and waits for a + * response from the server that includes the number of packets that the server received + * + * 2. UDPBurst Down: the device sends a request to a remote server on a UDP port and the server + * responds by sending a burst of UDPBurstCount packets. The size of each packet is packetSizeByte + */ +public class UDPBurstTask extends MeasurementTask { + public static final String TYPE = "udp_burst"; + public static final String DESCRIPTOR = "UDP Burst"; + + private static final int DEFAULT_PORT = 31341; + + /** + * Min packet size = (int type) + (int burstCount) + (int packetNum) + (int intervalNum) + (long + * timestamp) + (int packetSize) + (int seq) + (int udpInterval) = 36 + */ + private static final int MIN_PACKETSIZE = 36; + // Leave enough margin for min MTU in the link and IP options + private static final int MAX_PACKETSIZE = 500; + private static final int DEFAULT_UDP_PACKET_SIZE = 100; + /** + * Default number of packets to be sent + */ + private static final int DEFAULT_UDP_BURST = 16; + private static final int MAX_BURSTCOUNT = 100; + /** + * TODO(Hongyi): Interval between packets in millisecond level seems too long for regular UDP + * transmission. Microsecond level may be better. Need Discussion + */ + private static final int DEFAULT_UDP_INTERVAL = 1; + private static final int MAX_INTERVAL = 1; + + // TODO(Hongyi): choose a proper timeout period + private static final int RCV_UP_TIMEOUT = 2000; // round-trip delay, in msec. + private static final int RCV_DOWN_TIMEOUT = 1000; // one-way delay, in msec + + private static final int PKT_ERROR = 1; + private static final int PKT_RESPONSE = 2; + private static final int PKT_DATA = 3; + private static final int PKT_REQUEST = 4; + + private String targetIp = null; + private Context context = null; + + private static int seq = 1; + private long duration; + private TaskProgress taskProgress; + private volatile boolean stopFlag; + + // Track data consumption for this task to avoid exceeding user's limit + private long dataConsumed; + + + /** + * Encode UDP specific parameters, along with common parameters inherited from MeasurementDesc + * + */ + public static class UDPBurstDesc extends MeasurementDesc { + // Declare static parameters specific to SampleMeasurement here + + public int packetSizeByte = UDPBurstTask.DEFAULT_UDP_PACKET_SIZE; + public int udpBurstCount = UDPBurstTask.DEFAULT_UDP_BURST; + public int dstPort = UDPBurstTask.DEFAULT_PORT; + public String target = null; + public boolean dirUp = false; + public int udpInterval = UDPBurstTask.DEFAULT_UDP_INTERVAL; + + public UDPBurstDesc(String key, Date startTime, Date endTime, double intervalSec, long count, + long priority, int contextIntervalSec, Map params) + throws InvalidParameterException { + super(UDPBurstTask.TYPE, key, startTime, endTime, intervalSec, count, priority, + contextIntervalSec, params); + initializeParams(params); + if (this.target == null || this.target.length() == 0) { + throw new InvalidParameterException("UDPBurstTask null target"); + } + } + + /** + * There are three UDP specific parameters: + * + * 1. "direction": "up" if this is an uplink measurement. or "down" otherwise 2. "packet_burst": + * how many packets should a up/down burst have 3. "packet_size_byte": the size of each packet + * in bytes + */ + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + + if ((target = params.get("target")) == null) { + this.target = MLabNS.TARGET; + } + + try { + String val = null; + + if ((val = params.get("dst_port")) != null && val.length() > 0 && Integer.parseInt(val) > 0) { + this.dstPort = Integer.parseInt(val); + } + if ((val = params.get("packet_size_byte")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.packetSizeByte = Integer.parseInt(val); + if (this.packetSizeByte < MIN_PACKETSIZE) { + this.packetSizeByte = MIN_PACKETSIZE; + } + if (this.packetSizeByte > MAX_PACKETSIZE) { + this.packetSizeByte = MAX_PACKETSIZE; + } + } + if ((val = params.get("packet_burst")) != null && val.length() > 0 + && Integer.parseInt(val) > 0) { + this.udpBurstCount = Integer.parseInt(val); + if (this.udpBurstCount > MAX_BURSTCOUNT) { + this.udpBurstCount = MAX_BURSTCOUNT; + } + } + if ((val = params.get("udp_interval")) != null && val.length() > 0 + && Integer.parseInt(val) >= 0) { + this.udpInterval = Integer.parseInt(val); + if (this.udpInterval > MAX_INTERVAL) { + this.udpInterval = MAX_INTERVAL; + } + } + } catch (NumberFormatException e) { + throw new InvalidParameterException("UDPTask invalid params"); + } + + String dir = null; + if ((dir = params.get("direction")) != null && dir.length() > 0) { + if (dir.compareToIgnoreCase("Up") == 0) { + this.dirUp = true; + } + } + } + + @Override + public String getType() { + return UDPBurstTask.TYPE; + } + + protected UDPBurstDesc(Parcel in) { + super(in); + packetSizeByte = in.readInt(); + udpBurstCount = in.readInt(); + dstPort = in.readInt(); + target = in.readString(); + dirUp = in.readByte() != 0; + udpInterval = in.readInt(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public UDPBurstDesc createFromParcel(Parcel in) { + return new UDPBurstDesc(in); + } + + public UDPBurstDesc[] newArray(int size) { + return new UDPBurstDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(packetSizeByte); + dest.writeInt(udpBurstCount); + dest.writeInt(dstPort); + dest.writeString(target); + dest.writeByte((byte) (dirUp ? 1 : 0)); + dest.writeInt(udpInterval); + } + + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return UDPBurstDesc.class; + } + + public UDPBurstTask(MeasurementDesc desc) { + super(new UDPBurstDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters)); + this.taskProgress = TaskProgress.FAILED; + this.stopFlag = false; + this.duration = Config.DEFAULT_UDPBURST_DURATION; + this.dataConsumed = 0; + } + + protected UDPBurstTask(Parcel in) { + super(in); + taskProgress = (TaskProgress) in.readSerializable(); + stopFlag = in.readByte() != 0; + duration = in.readLong(); + dataConsumed = in.readLong(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public UDPBurstTask createFromParcel(Parcel in) { + return new UDPBurstTask(in); + } + + public UDPBurstTask[] newArray(int size) { + return new UDPBurstTask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeSerializable(taskProgress); + dest.writeByte((byte) (stopFlag ? 1 : 0)); + dest.writeLong(duration); + dest.writeLong(dataConsumed); + } + + /** + * Make a deep cloning of the task + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + UDPBurstDesc newDesc = + new UDPBurstDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters); + return new UDPBurstTask(newDesc); + } + + /** + * Opens a datagram (UDP) socket + * + * @return a datagram socket used for sending/receiving + * @throws MeasurementError if an error occurs + */ + private DatagramSocket openSocket() throws MeasurementError { + DatagramSocket sock = null; + + // Open datagram socket + try { + sock = new DatagramSocket(); + } catch (SocketException e) { + throw new MeasurementError("Socket creation failed"); + } + + return sock; + } + + /** + * @author Hongyi Yao (hyyao@umich.edu) This class encapsulates the results of UDP burst + * measurement + */ + private class UDPResult { + public int packetCount; + public double outOfOrderRatio; + public long jitter; + + public UDPResult() { + packetCount = 0; + outOfOrderRatio = 0.0; + jitter = 0L; + } + } + + /** + * @author Hongyi Yao (hyyao@umich.edu) This class calculates the out-of-order ratio and delay + * jitter in the array of received UDP packets + */ + private class MetricCalculator { + private int maxPacketNum; + private ArrayList offsetedDelayList; + private int packetCount; + private int outOfOrderCount; + + public MetricCalculator(int burstSize) { + maxPacketNum = -1; + offsetedDelayList = new ArrayList(); + packetCount = 0; + outOfOrderCount = 0; + } + + /** + * Out-of-order packets is defined as arriving packets with sequence numbers smaller than their + * predecessors. + * + * @param packetNum: packet number in burst sequence + * @param timestamp: estimated one-way delay(contains clock offset) + */ + public void addPacket(int packetNum, long timestamp) { + if (packetNum > maxPacketNum) { + maxPacketNum = packetNum; + } else { + outOfOrderCount++; + } + offsetedDelayList.add(System.currentTimeMillis() - timestamp); + packetCount++; + } + + /** + * Out-of-order ratio is defined as the ratio between the number of out-of-order packets and the + * total number of packets. + * + * @return the inversion number of the current UDP burst + */ + public double calculateOutOfOrderRatio() { + if (packetCount != 0) { + return (double) outOfOrderCount / packetCount; + } else { + return 0.0; + } + } + + /** + * Calculate jitter as the standard deviation of one-way delays[RFC3393] We can assume the clock + * offset between server and client is constant in a short period(several milliseconds) since + * typical oscillators have no more than 100ppm of frequency error , then it will be cancelled + * out during the calculation process + * + * @return the jitter of UDP burst + */ + public long calculateJitter() { + if (packetCount > 1) { + double offsetedDelay_mean = 0; + for (long offsetedDelay : offsetedDelayList) { + offsetedDelay_mean += (double) offsetedDelay / packetCount; + } + + double jitter = 0; + for (long offsetedDelay : offsetedDelayList) { + jitter += ((double) offsetedDelay - offsetedDelay_mean) + * ((double) offsetedDelay - offsetedDelay_mean) / (packetCount - 1); + } + jitter = Math.sqrt(jitter); + + return (long) jitter; + } else { + return 0; + } + } + } + /** + * @author Hongyi Yao (hyyao@umich.edu) A helper structure for packing and unpacking network + * message + */ + private class UDPPacket { + public int type; + public int burstCount; + public int packetNum; + public int outOfOrderNum; + // Data packet: local timestamp + // Response packet: jitter + public long timestamp; + public int packetSize; + public int seq; + public int udpInterval; + + /** + * Create an empty structure + * @param cliId corresponding client identifier + */ + public UDPPacket() {} + + /** + * Unpack received message and fill the structure + * + * @param cliId corresponding client identifier + * @param rawdata network message + * @throws MeasurementError stream reader failed + */ + public UDPPacket(byte[] rawdata) throws MeasurementError { + ByteArrayInputStream byteIn = new ByteArrayInputStream(rawdata); + DataInputStream dataIn = new DataInputStream(byteIn); + + try { + type = dataIn.readInt(); + burstCount = dataIn.readInt(); + packetNum = dataIn.readInt(); + outOfOrderNum = dataIn.readInt(); + timestamp = dataIn.readLong(); + packetSize = dataIn.readInt(); + seq = dataIn.readInt(); + udpInterval = dataIn.readInt(); + } catch (IOException e) { + throw new MeasurementError("Fetch payload failed! " + e.getMessage()); + } + + try { + byteIn.close(); + } catch (IOException e) { + throw new MeasurementError("Error closing inputstream!"); + } + } + + /** + * Pack the structure to the network message + * + * @return the network message in byte[] + * @throws MeasurementError stream writer failed + */ + public byte[] getByteArray() throws MeasurementError { + + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(byteOut); + + try { + dataOut.writeInt(type); + dataOut.writeInt(burstCount); + dataOut.writeInt(packetNum); + dataOut.writeInt(outOfOrderNum); + dataOut.writeLong(timestamp); + dataOut.writeInt(packetSize); + dataOut.writeInt(seq); + dataOut.writeInt(udpInterval); + } catch (IOException e) { + throw new MeasurementError("Create rawpacket failed! " + e.getMessage()); + } + + byte[] rawPacket = byteOut.toByteArray(); + + try { + byteOut.close(); + } catch (IOException e) { + throw new MeasurementError("Error closing outputstream!"); + } + return rawPacket; + } + + } + + /** + * Opens a Datagram socket to the server included in the UDPDesc and sends a burst of + * UDPBurstCount packets, each of size packetSizeByte. + * + * @return a Datagram socket that can be used to receive the server's response + * + * @throws MeasurementError if an error occurred. + */ + private DatagramSocket sendUpBurst() throws MeasurementError { + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + InetAddress addr = null; + // Resolve the server's name + try { + addr = InetAddress.getByName(desc.target); + dataConsumed+=DnsLookupTask.AVG_DATA_USAGE_BYTE; + targetIp = addr.getHostAddress(); + } catch (UnknownHostException e) { + throw new MeasurementError("Unknown host " + desc.target); + } + + DatagramSocket sock = null; + sock = openSocket(); + + UDPPacket dataPacket = new UDPPacket(); + // Send burst + for (int i = 0; i < desc.udpBurstCount; i++) { + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + dataPacket.type = UDPBurstTask.PKT_DATA; + dataPacket.burstCount = desc.udpBurstCount; + dataPacket.packetNum = i; + dataPacket.timestamp = System.currentTimeMillis(); + dataPacket.packetSize = desc.packetSizeByte; + dataPacket.seq = seq; + // Flatten UDP packet + byte[] data = dataPacket.getByteArray(); + + DatagramPacket packet = new DatagramPacket(data, data.length, addr, desc.dstPort); + + try { + sock.send(packet); + dataConsumed+=packet.getLength(); + } catch (IOException e) { + sock.close(); + throw new MeasurementError("Error sending " + desc.target); + } + Logger.i("Sent packet pnum:" + i + " to " + desc.target + ": " + targetIp); + // Sleep udpInterval millisecond + try { + Thread.sleep(desc.udpInterval); + Logger.i("UDP Burst sleep " + desc.udpInterval + "ms"); + } catch (InterruptedException e) { + Logger.e("UDPBurst -> sendUpBurst got interrupted"); + return null; + } + } // for() + return sock; + } + + /** + * Receive a response from the server after the burst of uplink packets was sent, parse it, and + * return the number of packets the server received. + * + * @param sock the socket used to receive the server's response + * @return the number of packets the server received + * + * @throws MeasurementError if an error or a timeout occurs + */ + private UDPResult recvUpResponse(DatagramSocket sock) throws MeasurementError { + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + + UDPResult udpResult = new UDPResult(); + // Receive response + Logger.i("Waiting for UDP response from " + desc.target + ": " + targetIp); + + byte buffer[] = new byte[UDPBurstTask.MIN_PACKETSIZE]; + DatagramPacket recvpacket = new DatagramPacket(buffer, buffer.length); + + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + try { + sock.setSoTimeout(RCV_UP_TIMEOUT); + sock.receive(recvpacket); + } catch (SocketException e1) { + sock.close(); + throw new MeasurementError("Timed out reading from " + desc.target); + } catch (IOException e) { + sock.close(); + throw new MeasurementError("Error reading from " + desc.target); + } + // Reconstruct UDP packet from flattened network data + UDPPacket responsePacket = new UDPPacket(recvpacket.getData()); + dataConsumed+=recvpacket.getLength(); + + if (responsePacket.type == PKT_RESPONSE) { + // Received seq number must be same with client seq + if (responsePacket.seq != seq) { + Logger.e("Error: Server send response packet with different seq, old " + seq + " => new " + + responsePacket.seq); + } + + Logger.i("Recv UDP resp from " + desc.target + " type:" + responsePacket.type + " burst:" + + responsePacket.burstCount + " pktnum:" + responsePacket.packetNum + + " out_of_order_num: " + responsePacket.outOfOrderNum + " jitter: " + + responsePacket.timestamp); + + udpResult.packetCount = responsePacket.packetNum; + udpResult.outOfOrderRatio = (double) responsePacket.outOfOrderNum / responsePacket.packetNum; + udpResult.jitter = responsePacket.timestamp; + return udpResult; + } else { + throw new MeasurementError("Error: not a response packet! seq: " + seq); + } + } + + /** + * Opens a datagram socket to the server in the UDPDesc and requests the server to send a burst of + * UDPBurstCount packets, each of packetSizeByte bytes. + * + * @return the datagram socket used to receive the server's burst + * @throws MeasurementError if an error occurs + */ + private DatagramSocket sendDownRequest() throws MeasurementError { + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + DatagramPacket packet; + InetAddress addr = null; + + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + + // Resolve the server's name + try { + addr = InetAddress.getByName(desc.target); + targetIp = addr.getHostAddress(); + } catch (UnknownHostException e) { + throw new MeasurementError("Unknown host " + desc.target); + } + + DatagramSocket sock = null; + sock = openSocket(); + + Logger.i("Requesting UDP burst:" + desc.udpBurstCount + " pktsize: " + desc.packetSizeByte + + " to " + desc.target + ": " + targetIp); + + UDPPacket requestPacket = new UDPPacket(); + requestPacket.type = PKT_REQUEST; + requestPacket.burstCount = desc.udpBurstCount; + requestPacket.packetSize = desc.packetSizeByte; + requestPacket.seq = seq; + requestPacket.udpInterval = desc.udpInterval; + // Flatten UDP packet + byte[] data = requestPacket.getByteArray(); + packet = new DatagramPacket(data, data.length, addr, desc.dstPort); + + + try { + dataConsumed += packet.getLength(); + sock.send(packet); + } catch (IOException e) { + sock.close(); + throw new MeasurementError("Error closing Output Stream to:" + desc.target); + } + return sock; + } + + /** + * Receives a burst from the remote server and counts the number of packets that were received. + * + * @param sock the datagram socket that can be used to receive the server's burst + * + * @return the number of packets received from the server + * @throws MeasurementError if an error occurs + */ + private UDPResult recvDownResponse(DatagramSocket sock) throws MeasurementError { + int pktRecv = 0; + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + + // Receive response + Logger.i("Waiting for UDP burst from " + desc.target); + // Reconstruct UDP packet from flattened network data + byte buffer[] = new byte[desc.packetSizeByte]; + DatagramPacket recvpacket = new DatagramPacket(buffer, buffer.length); + MetricCalculator metricCalculator = new MetricCalculator(desc.udpBurstCount); + for (int i = 0; i < desc.udpBurstCount; i++) { + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + try { + sock.setSoTimeout(RCV_DOWN_TIMEOUT); + sock.receive(recvpacket); + } catch (IOException e) { + Logger.e("Timeout at round " + i); + break; + } + + UDPPacket dataPacket = new UDPPacket(recvpacket.getData()); + dataConsumed+=recvpacket.getLength(); + if (dataPacket.type == UDPBurstTask.PKT_DATA) { + // Received seq number must be same with client seq + if (dataPacket.seq != seq) { + String err = + "Server send data packets with different seq, old " + seq + " => new " + + dataPacket.seq; + Logger.e(err); + throw new MeasurementError(err); + } + + Logger.i("Recv UDP response from " + desc.target + " type:" + dataPacket.type + " burst:" + + dataPacket.burstCount + " pktnum:" + dataPacket.packetNum + " timestamp:" + + dataPacket.timestamp); + + pktRecv++; + metricCalculator.addPacket(dataPacket.packetNum, dataPacket.timestamp); + } else { + throw new MeasurementError("Error closing input stream from " + desc.target); + } + } // for() + + UDPResult udpResult = new UDPResult(); + udpResult.packetCount = pktRecv; + udpResult.outOfOrderRatio = metricCalculator.calculateOutOfOrderRatio(); + udpResult.jitter = metricCalculator.calculateJitter(); + return udpResult; + } + + /** + * Depending on the type of measurement, indicated by desc.Up, perform an uplink/downlink + * measurement + * + * @return the measurement's results + * @throws MeasurementError if an error occurs + */ + @Override + public MeasurementResult[] call() throws MeasurementError { + DatagramSocket socket = null; + float response = 0.0F; + UDPResult udpResult; + int pktrecv = 0; + this.taskProgress = TaskProgress.FAILED; + + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + + if (!desc.target.equals(MLabNS.TARGET)) { + throw new InvalidParameterException("Unknown target " + desc.target + " for UDPBurstTask"); + } + + ArrayList mlabNSResult = MLabNS.Lookup(context, "mobiperf"); + if (mlabNSResult.size() == 1) { + desc.target = mlabNSResult.get(0); + } else { + throw new InvalidParameterException("Invalid MLabNS query result" + " for UDPBurstTask"); + } + Logger.i("Setting target to: " + desc.target); + + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + + Logger.i("Running UDPBurstTask on " + desc.target); + try { + if (desc.dirUp == true) { + socket = sendUpBurst(); + udpResult = recvUpResponse(socket); + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + pktrecv = udpResult.packetCount; + response = pktrecv / (float) desc.udpBurstCount; + this.taskProgress = TaskProgress.COMPLETED; + } else { + socket = sendDownRequest(); + udpResult = recvDownResponse(socket); + if (stopFlag) { + throw new MeasurementError("Cancelled"); + } + pktrecv = udpResult.packetCount; + response = pktrecv / (float) desc.udpBurstCount; + this.taskProgress = TaskProgress.COMPLETED; + } + } catch (MeasurementError e) { + throw e; + } finally { + // Update the sequence number to be used by the next burst. + // Hongyi: we should update seq number no matter the measurement is + // succeeded or not. It ensures previous last UDP burst's packets + // will not affect the current one. + seq++; + if (socket != null) { + socket.close(); + } + } + + MeasurementResult result = + new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + UDPBurstTask.TYPE, System.currentTimeMillis() * 1000, + this.taskProgress, this.measurementDesc); + + + result.addResult("target_ip", targetIp); + result.addResult("loss_ratio", 1.0 - response); + result.addResult("out_of_order_ratio", udpResult.outOfOrderRatio); + result.addResult("jitter", udpResult.jitter); + MeasurementResult[] mrArray = new MeasurementResult[1]; + mrArray[0] = result; + return mrArray; + + } + + @Override + public String getType() { + return UDPBurstTask.TYPE; + } + + /** + * Returns a brief human-readable descriptor of the task. + */ + @Override + public String getDescriptor() { + return UDPBurstTask.DESCRIPTOR; + } + + /** + * This will be printed to the device log console. Make sure it's well structured and human + * readable + */ + @Override + public String toString() { + UDPBurstDesc desc = (UDPBurstDesc) measurementDesc; + String resp; + + if (desc.dirUp) { + resp = "[UDPUp]\n"; + } else { + resp = "[UDPDown]\n"; + } + + resp += + " Target: " + desc.target + "\n Interval (sec): " + desc.intervalSec + "\n Next run: " + + desc.startTime; + + return resp; + } + + @Override + public long getDuration() { + return this.duration; + } + + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + + } + + /** + * Stop the measurement, even when it is running. Should release all acquired resource in this + * function. There should not be side effect if the measurement has not started or is already + * finished. + */ + @Override + public boolean stop() { + stopFlag = true; + return true; + } + + /** + * Based on a direct accounting of UDP packet sizes. + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } +} diff --git a/src/com/mobilyzer/measurements/VideoQoETask.java b/src/com/mobilyzer/measurements/VideoQoETask.java new file mode 100644 index 0000000..c0d54d4 --- /dev/null +++ b/src/com/mobilyzer/measurements/VideoQoETask.java @@ -0,0 +1,388 @@ +/* + * Copyright 2014 RobustNet Lab, University of Michigan. All Rights Reserved. + * + * 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 java.io.InvalidClassException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementResult; +import com.mobilyzer.MeasurementResult.TaskProgress; +import com.mobilyzer.MeasurementTask; +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.exceptions.MeasurementError; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.MeasurementJsonConvertor; +import com.mobilyzer.util.PhoneUtils; +import com.mobilyzer.util.video.VideoPlayerService; +import com.mobilyzer.util.video.util.DemoUtil; + +/** + * @author laoyao + * Measure the user-perceived Video QoE metrics by playing YouTube video in the background + */ +public class VideoQoETask extends MeasurementTask { + // Type name for internal use + public static final String TYPE = "video"; + // Human readable name for the task + public static final String DESCRIPTOR = "Video QoE"; + + private boolean isSucceed = false; + private int numFrameDropped; + private double initialLoadingTime; + private ArrayList rebufferTimes = new ArrayList(); + private ArrayList goodputTimestamps = new ArrayList(); + private ArrayList goodputValues = new ArrayList(); + private ArrayList goodputEstimateValues = new ArrayList(); + private ArrayList bitrateTimestamps = new ArrayList(); + private ArrayList bitrateValues = new ArrayList(); + private long bbaSwitchTime; + private long dataConsumed; + + private boolean isResultReceived; + private long duration; + /** + * @author laoyao + * Parameters for Video QoE measurement + */ + public static class VideoQoEDesc extends MeasurementDesc { + // The url to retrieve video + public String contentURL; + // The content id for YouTube video + public String contentId; + // The ABR algorithm for video playback + public int contentType; + + public VideoQoEDesc(String key, Date startTime, Date endTime, double intervalSec, + long count, long priority, int contextIntervalSec, Map params) { + super(VideoQoETask.TYPE, key, startTime, endTime, intervalSec, count, priority, + contextIntervalSec, params); + initializeParams(params); + if (this.contentURL == null) { + throw new InvalidParameterException("Video QoE task cannot be created" + + " due to null video url string"); + } + if (this.contentType != DemoUtil.TYPE_DASH_VOD && this.contentType != DemoUtil.TYPE_PROGRESSIVE && this.contentType != DemoUtil.TYPE_BBA ) { + throw new InvalidParameterException("Video QoE task cannot be created" + + " due to invalid streaming algorithm: " + this.contentType); + } + } + + @Override + public String getType() { + return VideoQoETask.TYPE; + } + + @Override + protected void initializeParams(Map params) { + if (params == null) { + return; + } + String val = null; + + this.contentURL = params.get("content_url"); + this.contentId = params.get("content_id"); + if ((val = params.get("content_type")) != null && Integer.parseInt(val) >= 0) { + this.contentType = Integer.parseInt(val); + } + + } + + protected VideoQoEDesc(Parcel in) { + super(in); + contentURL = in.readString(); + contentId = in.readString(); + contentType = in.readInt(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VideoQoEDesc createFromParcel(Parcel in) { + return new VideoQoEDesc(in); + } + + public VideoQoEDesc[] newArray(int size) { + return new VideoQoEDesc[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(this.contentURL); + dest.writeString(this.contentId); + dest.writeInt(this.contentType); + } + } + + /** + * Constructor for video QoE measuremen task + * @param desc + */ + public VideoQoETask(MeasurementDesc desc) { + super(new VideoQoEDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, + desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); + bbaSwitchTime=-1; + dataConsumed=0; + } + + protected VideoQoETask(Parcel in) { + super(in); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VideoQoETask createFromParcel(Parcel in) { + return new VideoQoETask(in); + } + + public VideoQoETask[] newArray(int size) { + return new VideoQoETask[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#clone() + */ + @Override + public MeasurementTask clone() { + MeasurementDesc desc = this.measurementDesc; + VideoQoEDesc newDesc = + new VideoQoEDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, + desc.priority, desc.contextIntervalSec, desc.parameters); + return new VideoQoETask(newDesc); + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#call() + */ + @Override + public MeasurementResult[] call() throws MeasurementError { + Logger.d("Video QoE: measurement started"); + + MeasurementResult[] mrArray = new MeasurementResult[1]; + VideoQoEDesc taskDesc = (VideoQoEDesc) this.measurementDesc; + + Intent videoIntent = new Intent(PhoneUtils.getGlobalContext(), VideoPlayerService.class); + videoIntent.setData(Uri.parse(taskDesc.contentURL)); + videoIntent.putExtra(DemoUtil.CONTENT_ID_EXTRA, taskDesc.contentId); + videoIntent.putExtra(DemoUtil.CONTENT_TYPE_EXTRA, taskDesc.contentType); + PhoneUtils.getGlobalContext().startService(videoIntent); + + + IntentFilter filter = new IntentFilter(); + filter.addAction(UpdateIntent.VIDEO_MEASUREMENT_ACTION); + BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Logger.d("Video QoE: result received"); + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED)){ + isSucceed = intent.getBooleanExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED, false); + Logger.d("Is succeed: " + isSucceed); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED)){ + numFrameDropped = intent.getIntExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED, 0); + Logger.d("Num frame dropped: " + numFrameDropped); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME)){ + initialLoadingTime = intent.getDoubleExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME, 0.0); + Logger.d("Initial Loading Time: " + initialLoadingTime); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME)) { + double[] rebufferTimeArray = intent.getDoubleArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME); + for (double rebuffer : rebufferTimeArray) { + rebufferTimes.add(rebuffer); + } + Logger.d("Rebuffer Times: " + rebufferTimes); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP)) { + String[] goodputTimestampArray = intent.getStringArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP); + goodputTimestamps = new ArrayList(Arrays.asList(goodputTimestampArray)); + Logger.d("Goodput Timestamps: " + goodputTimestamps); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE)) { + double[] goodputValueArray = intent.getDoubleArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE); + for (double goodput : goodputValueArray) { + goodputValues.add(goodput); + } + Logger.d("Goodput Values: " + goodputValues); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE)) { + long[] goodputEstimateValueArray = intent.getLongArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE); + for (long estiamte : goodputEstimateValueArray) { + goodputEstimateValues.add(estiamte); + } + Logger.d("Goodput Estimated Values: " + goodputValues); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP)) { + String[] bitrateTimestampArray = intent.getStringArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP); + bitrateTimestamps = new ArrayList(Arrays.asList(bitrateTimestampArray)); + Logger.d("Bitrate Timestamps: " + bitrateTimestamps); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE)) { + int[] bitrateValueArray = intent.getIntArrayExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE); + for (int bitrate : bitrateValueArray) { + bitrateValues.add(bitrate); + } + Logger.d("Bitrate Values: " + bitrateValues); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME)){ + bbaSwitchTime=intent.getLongExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME, -1); + } + if (intent.hasExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BYTE_USED)){ + dataConsumed=intent.getLongExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BYTE_USED, 0); + Logger.d("Data consumed: " + dataConsumed); + } + isResultReceived = true; + } + }; + PhoneUtils.getGlobalContext().registerReceiver(broadcastReceiver, filter); + + + + for(int i=0;i<60*5;i++){ + if(isDone()){ + break; + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + Logger.e("Video QoE: result ready? " + this.isResultReceived); + PhoneUtils.getGlobalContext().unregisterReceiver(broadcastReceiver); + + if(isDone()){ + Logger.i("Video QoE: Successfully measured QoE data"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + VideoQoETask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.COMPLETED, this.measurementDesc); +// result.addResult(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED, isSucceed); + result.addResult("video_num_frame_dropped", this.numFrameDropped); + result.addResult("video_initial_loading_time", this.initialLoadingTime); + result.addResult("video_rebuffer_times", this.rebufferTimes); + result.addResult("video_goodput_times", this.goodputTimestamps); + result.addResult("video_goodput_values", this.goodputValues); + result.addResult("video_goodput_estimate_values", this.goodputEstimateValues); + result.addResult("video_bitrate_times", this.bitrateTimestamps); + result.addResult("video_bitrate_values", this.bitrateValues); + if(this.bbaSwitchTime!=-1){ + result.addResult("video_bba_switch_time", this.bbaSwitchTime); + } + + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + mrArray[0]=result; + }else{ + Logger.i("Video QoE: Video measurement not finished"); + PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils(); + MeasurementResult result = new MeasurementResult( + phoneUtils.getDeviceInfo().deviceId, + phoneUtils.getDeviceProperty(this.getKey()), + VideoQoETask.TYPE, System.currentTimeMillis() * 1000, + TaskProgress.FAILED, this.measurementDesc); +// result.addResult("error", "measurement timeout"); + Logger.i(MeasurementJsonConvertor.toJsonString(result)); + mrArray[0]=result; + } + +// PhoneUtils.getGlobalContext().stopService(new Intent(PhoneUtils.getGlobalContext(), PLTExecutorService.class)); + return mrArray; + } + + private boolean isDone() { + return isResultReceived; + } + + @SuppressWarnings("rawtypes") + public static Class getDescClass() throws InvalidClassException { + return VideoQoEDesc.class; + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#getDescriptor() + */ + @Override + public String getDescriptor() { + return VideoQoETask.DESCRIPTOR; + } + + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#getType() + */ + @Override + public String getType() { + return VideoQoETask.TYPE; + } + + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#stop() + */ + @Override + public boolean stop() { + // There is nothing we need to do to stop the video measurement + return false; + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#getDuration() + */ + @Override + public long getDuration() { + return this.duration; + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#setDuration(long) + */ + @Override + public void setDuration(long newDuration) { + if (newDuration < 0) { + this.duration = 0; + } else { + this.duration = newDuration; + } + } + + /* (non-Javadoc) + * @see com.mobilyzer.MeasurementTask#getDataConsumed() + */ + @Override + public long getDataConsumed() { + return dataConsumed; + } + +} diff --git a/src/com/mobilyzer/util/AndroidWebView.java b/src/com/mobilyzer/util/AndroidWebView.java new file mode 100644 index 0000000..e1ae928 --- /dev/null +++ b/src/com/mobilyzer/util/AndroidWebView.java @@ -0,0 +1,314 @@ +package com.mobilyzer.util; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import com.mobilyzer.UpdateIntent; +import com.squareup.okhttp.ConnectionPool; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Protocol; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.internal.Util; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Handler; +import android.webkit.SslErrorHandler; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +public class AndroidWebView extends WebView { + public static enum WebViewProtocol{ + SPDY, HTTP + } + + private static String USER_AGENT="Mozilla/5.0 (Linux; U; Android 4.3; en-us; SCH-I535 Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"; + + OkHttpClient client; + boolean spdyTest; + long startTimeFilter; + String url; + Context context; + long pageStartLoading; + WebViewProtocol protocol; +// private volatile ArrayList objsTimings; +// private volatile int totalByte; + public AndroidWebView(Context context, boolean spdyTest, WebViewProtocol protocol, long startTimeFilter, String url) { + super(context); + this.spdyTest=spdyTest; + this.context=context; + this.url=url; + this.protocol=protocol; + + + + this.startTimeFilter=startTimeFilter; + clearCache(true); + getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); + getSettings().setAppCacheEnabled(false); + getSettings().setJavaScriptEnabled(true); + getSettings().setUserAgentString(USER_AGENT); + context.deleteDatabase("webview.db"); + context.deleteDatabase("webviewCache.db"); + clearHistory(); + + client= new OkHttpClient(); + if(client.getConnectionPool()!=null && client.getConnectionPool().getConnectionCount()!=0){ + client.getConnectionPool().evictAll(); + } + + client.setConnectionPool(ConnectionPool.getDefault()); +// client.getCache().delete(); + + + if(spdyTest){ + if(protocol.equals(WebViewProtocol.HTTP)){ + client.setProtocols(Util.immutableList(Protocol.HTTP_1_1)); + }else{ + client.setProtocols(Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1)); + } + }else{ + client.setProtocols(Util.immutableList(Protocol.HTTP_1_1)); + } + + + TrustManager localTrustmanager = new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + + } + }; + + // Create SSLContext and set the socket factory as default + try { + SSLContext sslc = SSLContext.getInstance("SSL"); + sslc.init(null, new TrustManager[] { localTrustmanager }, + new SecureRandom()); + client.setSslSocketFactory(sslc.getSocketFactory()); + HttpsURLConnection.setDefaultSSLSocketFactory(sslc.getSocketFactory()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + + + setWebViewClient(new WebViewClient(){ + + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + handler.proceed(); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + Logger.d("ashkan_plt: Page started: "+url); + super.onPageStarted(view, url, favicon); + } + + @Override + public void onPageFinished(WebView view, String url) { + Logger.d("ashkan_plt: Page finished: "+url); + + + new Handler(){ + private WebView view; + + + public void init(WebView v){ + view=v; + postDelayed(new Runnable() { + @Override + public void run() { + + String js_code = "javascript:(\n function() { \n"; + js_code += " var result='';\n"; + js_code += " for(var prop in performance.timing){\n"; + js_code += " if(performance.timing.hasOwnProperty(prop)){\n"; + js_code += " result=prop+':'+performance.timing[prop]+'|'+result}}\n"; + js_code += " console.log('mobilyzer_navigation'+result);\n"; + js_code += " perfObj=window.performance.getEntriesByType('resource');\n"; + js_code += " all_res='';\n"; + js_code += " for(var prop in perfObj){if(perfObj.hasOwnProperty(prop)){\n"; + js_code += " res='';\n"; + js_code += " for(var index in perfObj[prop]){res=index+':'+perfObj[prop][index]+'|'+res;}\n"; + if(AndroidWebView.this.spdyTest){ + if(AndroidWebView.this.protocol.equals(WebViewProtocol.HTTP)){ + js_code += " all_res=all_res+'mobilyzer_resource|http|'+res;}}\n"; + }else{ + js_code += " all_res=all_res+'mobilyzer_resource|spdy|'+res;}}\n"; + } + }else{ + js_code += " all_res=all_res+'mobilyzer_resource|http|'+res;}}\n"; + } + js_code += " console.log(all_res);\n"; + js_code += " })()\n"; + view.loadUrl(js_code); + } + }, 20000); + } + + }.init(view); + super.onPageFinished(view, url); + } + + + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String urlStr) { + + long relStartTime; + if(urlStr.equals(AndroidWebView.this.url)){ + pageStartLoading=System.currentTimeMillis(); + relStartTime=0; + }else{ + relStartTime=System.currentTimeMillis()-pageStartLoading; + } + Logger.d("ashkan_plt: shouldInterceptRequest: "+urlStr+" "+relStartTime); + + + if (urlStr == null || urlStr.trim().equals("") || !(urlStr.startsWith("http") && !urlStr.startsWith("www"))|| urlStr.contains("|")){ + return super.shouldInterceptRequest(view, urlStr); + } + + + return new MyWebResourceResponse(urlStr); +// return super.shouldInterceptRequest(view, urlStr); + } + + }); + + setWebChromeClient(new WebChromeClient(){ + @Override + public void onConsoleMessage(String message, int lineNumber, String sourceID) { + + Logger.d("onConsoleMessage: "+message); + Intent newintent = new Intent(); + newintent.setAction((UpdateIntent.PLT_MEASUREMENT_ACTION)+AndroidWebView.this.startTimeFilter); + if(message.startsWith("mobilyzer_navigation")){ + + if(AndroidWebView.this.spdyTest){ + if(AndroidWebView.this.protocol.equals(WebViewProtocol.HTTP)){ + message=message.replace("mobilyzer_navigation", "mobilyzer_navigation|http"); + }else{ + message=message.replace("mobilyzer_navigation", "mobilyzer_navigation|spdy"); + } + }else{ + message=message.replace("mobilyzer_navigation", "mobilyzer_navigation|http"); + } + newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_NAV, message); +// newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_BYTE_USED, totalByte);//TODO + PhoneUtils.getGlobalContext().sendBroadcast(newintent); + Logger.d("ashkan_plt: onConsoleMessage: Broadcasting mobilyzer_navigation result "+message.length()); + + if(AndroidWebView.this.spdyTest && AndroidWebView.this.protocol.equals(WebViewProtocol.HTTP)){ + AndroidWebView spdyWebView=new AndroidWebView(AndroidWebView.this.context, true, WebViewProtocol.SPDY ,AndroidWebView.this.startTimeFilter, AndroidWebView.this.url); + spdyWebView.loadUrl(); + } + + AndroidWebView.this.destroyDrawingCache(); +// AndroidWebView.this.destroy(); + + + }else if(message.startsWith("mobilyzer_resource")){ + newintent.putExtra(UpdateIntent.PLT_TASK_PAYLOAD_RESULT_RES, message); + PhoneUtils.getGlobalContext().sendBroadcast(newintent); + Logger.d("ashkan_plt: onConsoleMessage: Broadcasting mobilyzer_resource result "+message.length()); + } + } + }); + } + + class MyWebResourceResponse extends WebResourceResponse{ + + + private String url; + public MyWebResourceResponse(String url){ + super("", "", null); + this.url=url; + } + public MyWebResourceResponse(String mimeType, String encoding, + InputStream data) { + super(mimeType, encoding, data); + } + @Override + public InputStream getData() { + return new MyInputStream(url); + + + } + + } + + class MyInputStream extends InputStream{ + private String url; + private boolean initialized; + private InputStream is; + public MyInputStream(String url) { + this.url=url; + initialized=false; + } + @Override + public int read() throws IOException { + if(!initialized){ + try { + + Request request = new Request.Builder() + .url(url) + .header("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SCH-I535 Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30") + .build(); + + + long startTime=System.currentTimeMillis(); + Response response = client.newCall(request).execute(); + Logger.d("ashkan_plt: HTTP: "+client.getConnectionPool().getHttpConnectionCount()+" SPDY: "+client.getConnectionPool().getSpdyConnectionCount()); + long endTime=System.currentTimeMillis(); + is= response.body().byteStream(); + + Logger.d("ashkan_plt: load time for "+url+":\t "+(endTime-startTime)); + } catch (IOException e) { + e.printStackTrace(); + } + initialized=true; + } + return is.read(); + } + + } + + + public void loadUrl() { + super.loadUrl(this.url); + } + +} diff --git a/src/com/mobilyzer/util/Logger.java b/src/com/mobilyzer/util/Logger.java new file mode 100755 index 0000000..069a6ec --- /dev/null +++ b/src/com/mobilyzer/util/Logger.java @@ -0,0 +1,76 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.mobilyzer.util; + +import com.mobilyzer.Config; + +import android.util.Log; + +/** + * Wrapper for logging operations which can be disabled by setting LOGGING_ENABLED. + */ +public class Logger { + private final static boolean LOGGING_ENABLED = true; + private final static String TAG = "Mobilyzer-" + Config.version; + + public static void d(String msg) { + if (LOGGING_ENABLED) { + Log.d(TAG, msg); + } + } + + public static void d(String msg, Throwable t) { + if (LOGGING_ENABLED) { + Log.d(TAG, msg, t); + } + } + + public static void e(String msg) { + if (LOGGING_ENABLED) { + Log.e(TAG, msg); + } + } + + public static void e(String msg, Throwable t) { + if (LOGGING_ENABLED) { + Log.e(TAG, msg, t); + } + } + + public static void i(String msg) { + if (LOGGING_ENABLED) { + Log.i(TAG, msg); + } + } + + public static void i(String msg, Throwable t) { + if (LOGGING_ENABLED) { + Log.i(TAG, msg, t); + } + } + + public static void v(String msg) { + if (LOGGING_ENABLED) { + Log.v(TAG, msg); + } + } + + public static void v(String msg, Throwable t) { + if (LOGGING_ENABLED) { + Log.v(TAG, msg, t); + } + } + + public static void w(String msg) { + if (LOGGING_ENABLED) { + Log.w(TAG, msg); + } + } + + public static void w(String msg, Throwable t) { + if (LOGGING_ENABLED) { + Log.w(TAG, msg, t); + } + } + +} diff --git a/src/com/mobilyzer/util/MLabNS.java b/src/com/mobilyzer/util/MLabNS.java new file mode 100755 index 0000000..e46c83e --- /dev/null +++ b/src/com/mobilyzer/util/MLabNS.java @@ -0,0 +1,216 @@ +package com.mobilyzer.util; + +import android.content.Context; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.Reader; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.zip.GZIPInputStream; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.ParseException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HTTP; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class MLabNS { + + /** Used by measurement tests if MLabNS should be used + * to retrieve the real server target. */ + static public final String TARGET = "mlab"; + + /** + * Query MLab-NS to get an FQDN for the given tool. + */ + static public ArrayList Lookup(Context context, String tool) { + return Lookup(context, tool, null, "fqdn"); + } + + /** + * Query MLab-NS to get an FQDN/IP for the given tool and address family. + * @param field: fqdn or ip + */ + static public ArrayList Lookup(Context context, String tool, + String address_family, String field) { + final int maxResponseSize = 1024; + // Set the timeout in milliseconds until a connection is established. + final int timeoutConnection = 5000; + // Set the socket timeout in milliseconds. + final int timeoutSocket = 5000; + + ByteBuffer body = ByteBuffer.allocate(maxResponseSize); + InputStream inputStream = null; + + // Sanitize for possible returned field + if ( field != "fqdn" && field != "ip" ) { + return null; + } + + try { + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + DefaultHttpClient httpClient = new DefaultHttpClient(httpParameters); + + Logger.d("Creating request GET for mlab-ns"); + String url = "http://mlab-ns.appspot.com/" + tool + "?format=json"; + if (address_family == "ipv4" || address_family == "ipv6") { + url += "&address_family=" + address_family; + } + HttpGet request = new HttpGet(url); + request.setHeader("User-Agent", Util.prepareUserAgent()); + HttpResponse response = httpClient.execute(request); + if (response.getStatusLine().getStatusCode() != 200) { + throw new InvalidParameterException( + "Received status " + response.getStatusLine().getStatusCode() + + " from mlab-ns"); + } + Logger.d("STATUS OK"); + + String body_str = getResponseBody(response); + JSONObject json = new JSONObject(body_str); + Logger.d("Field Type is " + json.get(field).getClass().getName()); + ArrayList mlabNSResult = new ArrayList(); + if (json.get(field) instanceof JSONArray) { + // Convert array value into ArrayList + JSONArray jsonArray = (JSONArray)json.get(field); + for (int i = 0; i < jsonArray.length(); i++) { + mlabNSResult.add(jsonArray.get(i).toString()); + } + } else if (json.get(field) instanceof String) { + // Append the string into ArrayList + mlabNSResult.add(String.valueOf(json.getString(field))); + } else { + throw new InvalidParameterException("Unknown type " + + json.get(field).getClass().toString() + " of value " + json.get(field)); + } + return mlabNSResult; + } catch (SocketTimeoutException e) { + Logger.e("SocketTimeoutException trying to contact m-lab-ns"); + // e.getMessage() is null + throw new InvalidParameterException("Connect to m-lab-ns timeout. " + + "Please try again."); + } catch (IOException e) { + Logger.e("IOException trying to contact m-lab-ns: " + e.getMessage()); + throw new InvalidParameterException(e.getMessage()); + } catch (JSONException e) { + Logger.e("JSONException trying to contact m-lab-ns: " + e.getMessage()); + throw new InvalidParameterException(e.getMessage()); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Logger.e("Failed to close the input stream from the HTTP response"); + } + } + } + } + + static private String getContentCharSet(final HttpEntity entity) + throws ParseException { + if (entity == null) { + throw new IllegalArgumentException("entity may not be null"); + } + + String charset = null; + if (entity.getContentType() != null) { + HeaderElement values[] = entity.getContentType().getElements(); + if (values.length > 0) { + NameValuePair param = values[0].getParameterByName("charset"); + if (param != null) { + charset = param.getValue(); + } + } + } + return charset; + } + + static private String getResponseBodyFromEntity(HttpEntity entity) + throws IOException, ParseException { + if (entity == null) { + throw new IllegalArgumentException("entity may not be null"); + } + + InputStream instream = entity.getContent(); + if (instream == null) { + return ""; + } + + if (entity.getContentEncoding() != null) { + if ("gzip".equals(entity.getContentEncoding().getValue())) { + instream = new GZIPInputStream(instream); + } + } + + if (entity.getContentLength() > Integer.MAX_VALUE) { + throw new IllegalArgumentException("HTTP entity too large to be " + + "buffered into memory"); + } + + String charset = getContentCharSet(entity); + if (charset == null) { + charset = HTTP.DEFAULT_CONTENT_CHARSET; + } + + Reader reader = new InputStreamReader(instream, charset); + StringBuilder buffer = new StringBuilder(); + + try { + char[] tmp = new char[1024]; + int l; + while ((l = reader.read(tmp, 0, tmp.length)) != -1) { + Logger.d(" reading: " + tmp); + buffer.append(tmp); + } + } finally { + reader.close(); + } + + return buffer.toString(); + } + + static private String getResponseBody(HttpResponse response) + throws IllegalArgumentException { + String response_text = null; + HttpEntity entity = null; + + if (response == null) { + throw new IllegalArgumentException("response may not be null"); + } + + try { + entity = response.getEntity(); + response_text = getResponseBodyFromEntity(entity); + } catch (ParseException e) { + e.printStackTrace(); + } catch (IOException e) { + if (entity != null) { + try { + entity.consumeContent(); + } catch (IOException e1) { + } + } + } + return response_text; + } +} diff --git a/src/com/mobilyzer/util/MeasurementJsonConvertor.java b/src/com/mobilyzer/util/MeasurementJsonConvertor.java new file mode 100755 index 0000000..038c390 --- /dev/null +++ b/src/com/mobilyzer/util/MeasurementJsonConvertor.java @@ -0,0 +1,154 @@ +/* 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.util; + +import com.google.myjson.FieldNamingPolicy; +import com.google.myjson.Gson; +import com.google.myjson.GsonBuilder; +import com.google.myjson.JsonDeserializationContext; +import com.google.myjson.JsonDeserializer; +import com.google.myjson.JsonElement; +import com.google.myjson.JsonParseException; +import com.google.myjson.JsonPrimitive; +import com.google.myjson.JsonSerializationContext; +import com.google.myjson.JsonSerializer; +import com.mobilyzer.MeasurementDesc; +import com.mobilyzer.MeasurementTask; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + + +/** + * Utility class that use the gson library to provide bidirectional conversion + * between measurement objects (descriptions, tasks, and results, etc.) and + * JSON objects. New types of MeasurementDesc should be registered in the static + * HashMap initialization section. + */ +@SuppressWarnings("rawtypes") +public class MeasurementJsonConvertor { + /* 1. Automatically perform bidirectional translations for fields in java + * 'lowerCaseCamel' style + * to JSON 'lower_case_with_underscores' style. + * 2. Serialize and de-serialize UTC format date string + * 3. It also serializes all null fields to 'null' + */ + public static Gson gson = new GsonBuilder().serializeNulls(). + registerTypeAdapter(Date.class, new DateTypeConverter()). + setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + private static final DateFormat dateFormat = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + static { + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + public static MeasurementTask makeMeasurementTaskFromJson( + JSONObject json) throws IllegalArgumentException { + try { + String type = String.valueOf(json.getString("type")); + Class taskClass = MeasurementTask.getTaskClassForMeasurement(type); + Method getDescMethod = taskClass.getMethod("getDescClass"); + // The getDescClassForMeasurement() is static and takes no arguments + Class descClass = (Class) getDescMethod.invoke(null, (Object[]) null); + MeasurementDesc measurementDesc = + (MeasurementDesc) gson.fromJson(json.toString(), descClass); + + Object cstParam = measurementDesc; + Constructor constructor = + taskClass.getConstructor(MeasurementDesc.class); + return constructor.newInstance(cstParam); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } catch (SecurityException e) { + Logger.w(e.getMessage()); + throw new IllegalArgumentException(e); + } catch (NoSuchMethodException e) { + Logger.w(e.getMessage()); + throw new IllegalArgumentException(e); + } catch (IllegalArgumentException e) { + Logger.w(e.getMessage()); + throw new IllegalArgumentException(e); + } catch (InstantiationException e) { + Logger.w(e.getMessage()); + throw new IllegalArgumentException(e); + } catch (IllegalAccessException e) { + Logger.w(e.getMessage()); + throw new IllegalArgumentException(e); + } catch (InvocationTargetException e) { + Logger.w(e.toString()); + throw new IllegalArgumentException(e); + } + } + + public static JSONObject encodeToJson(Object obj) throws JSONException { + String str = gson.toJson(obj); + return new JSONObject(str); + } + + public static String toJsonString(Object obj) { + return gson.toJson(obj); + } + + public static Gson getGsonInstance() { + return gson; + } + + private static class DateTypeConverter + implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize(Date src, Type srcType, + JsonSerializationContext context) { + return new JsonPrimitive(formatDate(src)); + } + + @Override + public Date deserialize(JsonElement json, Type type, + JsonDeserializationContext context) throws JsonParseException { + try { + return parseDate(json.getAsString()); + } catch (NumberFormatException e) { + throw new JsonParseException("Cannot convert time string: " + + json.toString()); + } catch (IllegalArgumentException e) { + Logger.e("Cannot convert time string:" + json.toString()); + throw new JsonParseException("Cannot convert time string: " + + json.toString()); + } catch (ParseException e) { + throw new JsonParseException("Cannot convert UTC time string: " + + json.toString()); + } + } + } + + private static Date parseDate(String dateString) throws ParseException { + return dateFormat.parse(dateString); + } + + private static String formatDate(Date date) { + return dateFormat.format(date); + } +} diff --git a/src/com/mobilyzer/util/PhoneUtils.java b/src/com/mobilyzer/util/PhoneUtils.java new file mode 100755 index 0000000..4876154 --- /dev/null +++ b/src/com/mobilyzer/util/PhoneUtils.java @@ -0,0 +1,1029 @@ +/* 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.util; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.provider.Settings.Secure; +import android.telephony.CellInfo; +import android.telephony.CellInfoCdma; +import android.telephony.CellInfoGsm; +import android.telephony.CellInfoLte; +import android.telephony.NeighboringCellInfo; +import android.telephony.PhoneStateListener; +import android.telephony.SignalStrength; +import android.telephony.TelephonyManager; +import android.view.Display; +import android.view.WindowManager; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteOrder; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import com.mobilyzer.Config; +import com.mobilyzer.DeviceInfo; +import com.mobilyzer.DeviceProperty; +import com.mobilyzer.R; + + +/** + * Phone related utilities. + */ +@SuppressLint("NewApi") +public class PhoneUtils { + + private static final String ANDROID_STRING = "Android"; + /** Returned by {@link #getNetwork()}. */ + public static final String NETWORK_WIFI = "Wifi"; + /** IP type */ + public static final String IP_TYPE_UNKNOWN = "UNKNOWN"; + public static final String IP_TYPE_NONE = "Neither IPv4 nor IPv6"; + public static final String IP_TYPE_IPV4_ONLY = "IPv4 only"; + public static final String IP_TYPE_IPV6_ONLY = "IPv6 only"; + public static final String IP_TYPE_IPV4_IPV6_BOTH = "IPv4 and IPv6"; + + public static volatile HashSet clientKeySet + = new HashSet(); + /** + * The app that uses this class. The app must remain alive for longer than + * PhoneUtils objects are in use. + * + * @see #setGlobalContext(Context) + */ + private static Context globalContext = null; + + /** A singleton instance of PhoneUtils. */ + private static PhoneUtils singletonPhoneUtils = null; + + /** Phone context object giving access to various phone parameters. */ + private Context context = null; + + /** Allows to obtain the phone's location, to determine the country. */ + private LocationManager locationManager = null; + + /** The name of the location provider with "coarse" precision (cell/wifi). */ + private String locationProviderName = null; + + /** Allows to disable going to low-power mode where WiFi gets turned off. */ + WakeLock wakeLock = null; + + /** Call initNetworkManager() before using this var. */ + private ConnectivityManager connectivityManager = null; + + /** Call initNetworkManager() before using this var. */ + private TelephonyManager telephonyManager = null; + + /** Tells whether the phone is charging */ + private boolean isCharging; + /** Current battery level in percentage */ + private int curBatteryLevel; + /** Receiver that handles battery change broadcast intents */ + private BroadcastReceiver broadcastReceiver; + private String currentSignalStrength = "UNKNOWN"; + + /** For monitoring the current network connection type**/ + public static int TYPE_WIFI = 1; + public static int TYPE_MOBILE = 2; + public static int TYPE_NOT_CONNECTED = 0; + private int currentNetworkConnection= TYPE_NOT_CONNECTED; + + + private DeviceInfo deviceInfo = null; + /** IP compatibility status */ + // Indeterministic type due to client side timer expired + private int IP_TYPE_CANNOT_DECIDE = 2; + // Cannot resolve the hostname or cannot reach the destination address + private int IP_TYPE_UNCONNECTIVITY = 1; + private int IP_TYPE_CONNECTIVITY = 0; + /** Domain name resolution status */ + private int DN_UNKNOWN = 2; + private int DN_UNRESOLVABLE = 1; + private int DN_RESOLVABLE = 0; + //server configuration port on M-Lab servers + private int portNum = 6003; + private int tcpTimeout = 3000; + + protected PhoneUtils(Context context) { + this.context = context; + broadcastReceiver = new PowerStateChangeReceiver(); + // Registers a receiver for battery change events. + Intent powerIntent = globalContext.registerReceiver(broadcastReceiver, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + updateBatteryStat(powerIntent); + } + + /** + * The owner app class must call this method from its onCreate(), before + * getPhoneUtils(). + */ + public static synchronized void setGlobalContext(Context newGlobalContext) { + assert newGlobalContext != null; + assert singletonPhoneUtils == null; // Should not yet be created + // Not supposed to change the owner app + assert globalContext == null || globalContext == newGlobalContext; + + globalContext = newGlobalContext; + } + + public static synchronized void releaseGlobalContext() { + globalContext = null; + singletonPhoneUtils = null; + } + + /** Returns the context previously set with {@link #setGlobalContext}. */ + public static synchronized Context getGlobalContext() { + assert globalContext != null; + return globalContext; + } + + /** + * Returns a singleton instance of PhoneUtils. The caller must call + * {@link #setGlobalContext(Context)} before calling this method. + */ + public static synchronized PhoneUtils getPhoneUtils() { + + if (singletonPhoneUtils == null) { + assert globalContext != null; + singletonPhoneUtils = new PhoneUtils(globalContext); + } + + return singletonPhoneUtils; + } + + /** + * Returns a string representing this phone: + * + * "Android_-__" + + * "__" + * + * hardware-type is e.g. "dream", "passion", "emulator", etc. + * build-release is the SDK public release number e.g. "2.0.1" for Eclair. + * network-type is e.g. "Wifi", "Edge", "UMTS", "3G". + * network-carrier is the mobile carrier name if connected via the SIM card, + * or the Wi-Fi SSID if connected via the Wi-Fi. + * mobile-type is the phone's mobile network connection type -- "GSM" or "CDMA". + * + * If the device screen is currently in lanscape mode, "_Landscape" is + * appended at the end. + * + * TODO(klm): This needs to be converted into named URL args from positional, + * both here and in the iPhone app. Otherwise it's hard to add extensions, + * especially if there is optional stuff like + * + * @return a string representing this phone + */ + public String generatePhoneId() { + String device = Build.DEVICE.equals("generic") ? "emulator" : Build.DEVICE; + String network = getNetwork(); + String carrier = (network == NETWORK_WIFI) ? + getWifiCarrierName() : getTelephonyCarrierName(); + + StringBuilder stringBuilder = new StringBuilder(ANDROID_STRING); + stringBuilder.append('-').append(device).append('_') + .append(Build.VERSION.RELEASE).append('_').append(network) + .append('_').append(carrier).append('_').append(getTelephonyPhoneType()) + .append('_').append(isLandscape() ? "Landscape" : "Portrait"); + + return stringBuilder.toString(); + } + + /** + * Lazily initializes the network managers. + * + * As a side effect, assigns connectivityManager and telephonyManager. + */ + private synchronized void initNetwork() { + if (connectivityManager == null) { + ConnectivityManager tryConnectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + TelephonyManager tryTelephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + // Assign to member vars only after all the get calls succeeded, + // so that either all get assigned, or none get assigned. + connectivityManager = tryConnectivityManager; + telephonyManager = tryTelephonyManager; + + // Some interesting info to look at in the logs + NetworkInfo[] infos = connectivityManager.getAllNetworkInfo(); + for (NetworkInfo networkInfo : infos) { + Logger.i("Network: " + networkInfo); + } + Logger.i("Phone type: " + getTelephonyPhoneType() + + ", Carrier: " + getTelephonyCarrierName()); + } + assert connectivityManager != null; + assert telephonyManager != null; + } + + /** + * This method must be called in the service thread, as the system will create a Looper in + * the calling thread which will handle the callbacks. + */ + public void registerSignalStrengthListener() { + initNetwork(); + telephonyManager.listen(new SignalStrengthChangeListener(), + PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); + } + + /** Returns the network that the phone is on (e.g. Wifi, Edge, GPRS, etc). */ + public String getNetwork() { + initNetwork(); + NetworkInfo networkInfo = + connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (networkInfo != null && + networkInfo.getState() == NetworkInfo.State.CONNECTED) { + Logger.d("Current Network: WIFI"); + return NETWORK_WIFI; + } else { + return getTelephonyNetworkType(); + } + } + /** Detect whether there is an Internet connection available */ + public boolean isNetworkAvailable() { + initNetwork(); + NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); + return activeNetworkInfo != null && activeNetworkInfo.isConnected(); + } + + // /** Returns the WiFi network state (NetworkInfo.State) */ + // public NetworkInfo.State getNetworkState() { + // initNetwork(); + // NetworkInfo networkInfo = + // connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + // return networkInfo.getState(); + // } + + private static final String[] NETWORK_TYPES = { + "UNKNOWN", // 0 - NETWORK_TYPE_UNKNOWN + "GPRS", // 1 - NETWORK_TYPE_GPRS + "EDGE", // 2 - NETWORK_TYPE_EDGE + "UMTS", // 3 - NETWORK_TYPE_UMTS + "CDMA", // 4 - NETWORK_TYPE_CDMA + "EVDO_0", // 5 - NETWORK_TYPE_EVDO_0 + "EVDO_A", // 6 - NETWORK_TYPE_EVDO_A + "1xRTT", // 7 - NETWORK_TYPE_1xRTT + "HSDPA", // 8 - NETWORK_TYPE_HSDPA + "HSUPA", // 9 - NETWORK_TYPE_HSUPA + "HSPA", // 10 - NETWORK_TYPE_HSPA + "IDEN", // 11 - NETWORK_TYPE_IDEN + "EVDO_B", // 12 - NETWORK_TYPE_EVDO_B + "LTE", // 13 - NETWORK_TYPE_LTE + "EHRPD", // 14 - NETWORK_TYPE_EHRPD + "HSPAP", // 15 - NETWORK_TYPE_HSPAP + }; + + /** Returns mobile data network connection type. */ + private String getTelephonyNetworkType() { + assert NETWORK_TYPES[14].compareTo("EHRPD") == 0; + + int networkType = telephonyManager.getNetworkType(); + if (networkType < NETWORK_TYPES.length) { + return NETWORK_TYPES[telephonyManager.getNetworkType()]; + } else { + return "Unrecognized: " + networkType; + } + } + + /** Returns "GSM", "CDMA". */ + private String getTelephonyPhoneType() { + switch (telephonyManager.getPhoneType()) { + case TelephonyManager.PHONE_TYPE_CDMA: + return "CDMA"; + case TelephonyManager.PHONE_TYPE_GSM: + return "GSM"; + case TelephonyManager.PHONE_TYPE_NONE: + return "None"; + } + return "Unknown"; + } + + /** Returns current mobile phone carrier name, or empty if not connected. */ + private String getTelephonyCarrierName() { + return telephonyManager.getNetworkOperatorName(); + } + + /** Returns current Wi-Fi SSID, or null if Wi-Fi is not connected. */ + private String getWifiCarrierName() { + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + return wifiInfo.getSSID(); + } + return null; + } + + /** + * Returns the information about cell towers in range. Returns null if the information is + * not available + * + * TODO(wenjiezeng): As folklore has it and Wenjie has confirmed, we cannot get cell info from + * Samsung phones. + */ + public String getCellInfo(boolean cidOnly) { + initNetwork(); + List infos = telephonyManager.getNeighboringCellInfo(); + StringBuffer buf = new StringBuffer(); + String tempResult = ""; + if (infos.size() > 0) { + for (NeighboringCellInfo info : infos) { + tempResult = cidOnly ? info.getCid() + ";" : info.getLac() + "," + + info.getCid() + "," + info.getRssi() + ";"; + buf.append(tempResult); + } + // Removes the trailing semicolon + buf.deleteCharAt(buf.length() - 1); + return buf.toString(); + } else { + return null; + } + } + + /** + * Lazily initializes the location manager. + * + * As a side effect, assigns locationManager and locationProviderName. + */ + private synchronized void initLocation() { + if (locationManager == null) { + LocationManager manager = + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + Criteria criteriaCoarse = new Criteria(); + /* "Coarse" accuracy means "no need to use GPS". + * Typically a gShots phone would be located in a building, + * and GPS may not be able to acquire a location. + * We only care about the location to determine the country, + * so we don't need a super accurate location, cell/wifi is good enough. + */ + criteriaCoarse.setAccuracy(Criteria.ACCURACY_COARSE); + criteriaCoarse.setPowerRequirement(Criteria.POWER_LOW); + String providerName = + manager.getBestProvider(criteriaCoarse, /*enabledOnly=*/true); + + + List providers = manager.getAllProviders(); + for (String providerNameIter : providers) { + try { + LocationProvider provider = manager.getProvider(providerNameIter); + } catch (SecurityException se) { + // Not allowed to use this provider + Logger.w("Unable to use provider " + providerNameIter); + continue; + } + Logger.i(providerNameIter + ": " + + (manager.isProviderEnabled(providerNameIter) ? "enabled" : "disabled")); + } + + /* Make sure the provider updates its location. + * Without this, we may get a very old location, even a + * device powercycle may not update it. + * {@see android.location.LocationManager.getLastKnownLocation}. + */ + manager.requestLocationUpdates(providerName, + /*minTime=*/0, + /*minDistance=*/0, + new LoggingLocationListener(), + Looper.getMainLooper()); + locationManager = manager; + locationProviderName = providerName; + } + assert locationManager != null; + assert locationProviderName != null; + } + + /** + * Returns the location of the device. + * + * @return the location of the device + */ + public Location getLocation() { + try { + initLocation(); + Location location = locationManager.getLastKnownLocation(locationProviderName); + if (location == null) { + Logger.e("Cannot obtain location from provider " + locationProviderName); + return new Location("unknown"); + } + return location; + } catch (IllegalArgumentException e) { + Logger.e("Cannot obtain location", e); + return new Location("unknown"); + } + } + + /** Wakes up the CPU of the phone if it is sleeping. */ + public synchronized void acquireWakeLock() { + if (wakeLock == null) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "tag"); + } + Logger.d("PowerLock acquired"); + wakeLock.acquire(); + } + + /** Release the CPU wake lock. WakeLock is reference counted by default: no need to worry + * about releasing someone else's wake lock */ + public synchronized void releaseWakeLock() { + if (wakeLock != null) { + try { + wakeLock.release(); + Logger.i("PowerLock released"); + } catch (RuntimeException e) { + Logger.e("Exception when releasing wakeup lock", e); + } + } + } + + /** Release all resource upon app shutdown */ + public synchronized void shutDown() { + if (this.wakeLock != null) { + /* Wakelock are ref counted by default. We disable this feature here to ensure that + * the power lock is released upon shutdown. + */ + wakeLock.setReferenceCounted(false); + wakeLock.release(); + } + context.unregisterReceiver(broadcastReceiver); + releaseGlobalContext(); + } + + /** + * Returns true if the phone is in landscape mode. + */ + public boolean isLandscape() { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + return display.getWidth() > display.getHeight(); + } + + + + /** + * A dummy listener that just logs callbacks. + */ + private static class LoggingLocationListener implements LocationListener { + + @Override + public void onLocationChanged(Location location) { + Logger.d("location changed"); + } + + @Override + public void onProviderDisabled(String provider) { + Logger.d("provider disabled: " + provider); + } + + @Override + public void onProviderEnabled(String provider) { + Logger.d("provider enabled: " + provider); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + Logger.d("status changed: " + provider + "=" + status); + } + } + + /** + * Types of interfaces to return from {@link #getUpInterfaces(InterfaceType)}. + */ + public enum InterfaceType { + /** Local and external interfaces. */ + ALL, + + /** Only external interfaces. */ + EXTERNAL_ONLY, + } + + /** Returns a debug printable representation of a string list. */ + public static String debugString(List stringList) { + StringBuilder result = new StringBuilder("["); + Iterator listIter = stringList.iterator(); + if (listIter.hasNext()) { + result.append('"'); // Opening quote for the first string + result.append(listIter.next()); + while (listIter.hasNext()) { + result.append("\", \""); + result.append(listIter.next()); + } + result.append('"'); // Closing quote for the last string + } + result.append(']'); + return result.toString(); + } + + /** Returns a debug printable representation of a string array. */ + public static String debugString(String[] arr) { + return debugString(Arrays.asList(arr)); + } + + public String getAppVersionName() { + try { + String packageName = context.getPackageName(); + return context.getPackageManager().getPackageInfo(packageName, 0).versionName; + // String versionName = context.getString(R.string.scheduler_version_name); + // Logger.i("Scheduler: version name = " + versionName); + // return versionName; + } catch (Exception e) { + Logger.e("version name of the application cannot be found", e); + } + return "Unknown"; + } + + /** + * Returns the current battery level + * */ + public synchronized int getCurrentBatteryLevel() { + return curBatteryLevel; + } + + /** + * Returns if the batter is charing + */ + public synchronized boolean isCharging() { + return isCharging; + } + + /** + * Sets the current RSSI value + */ + public synchronized void setCurrentRssi(String rssi) { + currentSignalStrength = rssi; + } + + /** + * Returns the last updated RSSI value + */ + public synchronized String getCurrentRssi() { + initNetwork(); + return currentSignalStrength; + } + + public String getCellRssi(){ + String cellRssi1=getCurrentRssi(); + String cellRssi2=""; + HashMap cellInfosMap=getAllCellInfoSignalStrength(); + for (String cinfo: cellInfosMap.keySet()){ + cellRssi2+=cinfo+":"+cellInfosMap.get(cinfo)+"|"; + } + + return cellRssi1+cellRssi2; + + } + + + + + public String getWifiBSSID(){ + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + return wifiInfo.getBSSID(); + } + return null; + } + + public String getWifiSSID(){ + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + return wifiInfo.getSSID(); + } + return null; + } + + public String getWifiIpAddress(){ + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + int ip= wifiInfo.getIpAddress(); + if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { + ip = Integer.reverseBytes(ip); + } + byte[] bytes = BigInteger.valueOf(ip).toByteArray(); + String address; + try { + address = InetAddress.getByAddress(bytes).getHostAddress(); + return address; + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + } + return null; + } + + public HashMap getAllCellInfoSignalStrength(){ + TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + List cellInfos = (List) telephonyManager.getAllCellInfo(); + + HashMap results = new HashMap(); + + if(cellInfos==null){ + return results; + } + + for(CellInfo cellInfo : cellInfos){ + if(cellInfo.getClass().equals(CellInfoLte.class)){ + results.put("LTE", ((CellInfoLte) cellInfo).getCellSignalStrength().getDbm()); + }else if(cellInfo.getClass().equals(CellInfoGsm.class)){ + results.put("GSM", ((CellInfoGsm) cellInfo).getCellSignalStrength().getDbm()); + }else if(cellInfo.getClass().equals(CellInfoCdma.class)){ + results.put("CDMA", ((CellInfoCdma) cellInfo).getCellSignalStrength().getDbm()); + } + } + + return results; + } + + + + + public int getWifiRSSI(){ + WifiManager wifiManager = + (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + return wifiInfo.getRssi(); + } + return -1; + } + + + + + private synchronized void updateBatteryStat(Intent powerIntent) { + int scale = powerIntent.getIntExtra(BatteryManager.EXTRA_SCALE, + Config.DEFAULT_BATTERY_SCALE); + int level = powerIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, + Config.DEFAULT_BATTERY_LEVEL); + // change to the unit of percentage + this.curBatteryLevel = level * 100 / scale; + this.isCharging = powerIntent.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN) == BatteryManager.BATTERY_STATUS_CHARGING; + + Logger.i( + "Current power level is " + curBatteryLevel + " and isCharging = " + isCharging); + } + + private class PowerStateChangeReceiver extends BroadcastReceiver { + /** + * @see android.content.BroadcastReceiver#onReceive(android.content.Context, + * android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) { + updateBatteryStat(intent); + } + } + + private class SignalStrengthChangeListener extends PhoneStateListener { + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + String rssis=""; + + rssis+="CDMA:"+signalStrength.getCdmaDbm()+"|"; + rssis+="EVDO:"+signalStrength.getEvdoDbm()+"|"; + rssis+="GSM:"+signalStrength.getGsmSignalStrength()+"|"; + try { + Method[] methods = android.telephony.SignalStrength.class + .getMethods(); + for (Method mthd : methods) { + if (mthd.getName().equals("getLteDbm")){ + rssis+="LTE:"+(Integer) mthd.invoke(signalStrength)+"|"; + } + + } + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + + setCurrentRssi(rssis); + + } + } + + // /** + // * Fetches the new connectivity state from the connectivity manager directly. + // */ + // private synchronized void updateConnectivityInfo() { + // ConnectivityManager cm = (ConnectivityManager) context + // .getSystemService(Context.CONNECTIVITY_SERVICE); + // NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + // if (activeNetwork != null) { + // if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) { + // PhoneUtils.this.currentNetworkConnection = TYPE_WIFI; + // Logger.i("currentNetworkConnection: TYPE_WIFI"); + // } + // if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { + // PhoneUtils.this.currentNetworkConnection = TYPE_MOBILE; + // Logger.i("currentNetworkConnection: TYPE_MOBILE"); + // } + // } else { + // PhoneUtils.this.currentNetworkConnection = TYPE_NOT_CONNECTED; + // Logger.i("currentNetworkConnection: TYPE_NOT_CONNECTED"); + // } + // } + // + //TODO + // /** + // * When alerted that the network connectivity has changed, change the + // * stored connectivity value. + // */ + // private class ConnectivityChangeReceiver extends BroadcastReceiver { + // + //// public ConnectivityChangeReceiver() { + //// super(); + //// } + // + // @Override + // public void onReceive(Context context, Intent intent) { + // updateConnectivityInfo(); + // + // } + // } + // + // public synchronized int getCurrentNetworkConnection() { + // return currentNetworkConnection; + // } + + private String getVersionStr() { + return String.format("INCREMENTAL:%s, RELEASE:%s, SDK_INT:%s", + Build.VERSION.INCREMENTAL, Build.VERSION.RELEASE, Build.VERSION.SDK_INT); + } + + private String getDeviceId() { + // This ID is permanent to a physical phone. + String deviceId = telephonyManager.getDeviceId(); + + + // "generic" means the emulator. + if (deviceId == null || Build.DEVICE.equals("generic")) { + + // This ID changes on OS reinstall/factory reset. + deviceId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); + } + + return deviceId; + } + + + public DeviceInfo getDeviceInfo() { + + if (deviceInfo == null) { + deviceInfo = new DeviceInfo(); + deviceInfo.deviceId = getDeviceId(); + deviceInfo.manufacturer = Build.MANUFACTURER; + deviceInfo.model = Build.MODEL; + deviceInfo.os = getVersionStr(); + deviceInfo.user = Build.VERSION.CODENAME; + } + + return deviceInfo; + } + + private Location getMockLocation() { + return new Location("MockProvider"); + } + + // Hongyi: it is not a good idea to hard code here. Instead we can move those + // strings from string.xml to Config.java + public String getServerUrl() { + return Config.SERVER_URL; + } + + public String getAnonymousServerUrl() { + return Config.ANONYMOUS_SERVER_URL; + } + + public String getTestingServerUrl() { + return Config.TEST_SERVER_URL; + } + + public boolean isTestingServer(String serverUrl) { + return serverUrl == getTestingServerUrl(); + } + + /** + * Using MLab service to detect ipv4 or ipv6 compatibility + * @param ip_detect_type -- "ipv4" or "ipv6" + * @return IP_TYPE_CANNOT_DECIDE, IP_TYPE_UNCONNECTIVITY, IP_TYPE_CONNECTIVITY + */ + private int checkIPCompatibility(String ip_detect_type) { + if (!ip_detect_type.equals("ipv4") && !ip_detect_type.equals("ipv6")) { + return IP_TYPE_CANNOT_DECIDE; + } + Socket tcpSocket = new Socket(); + try { + ArrayList hostnameList = MLabNS.Lookup(context, "mobiperf", + ip_detect_type, "ip"); + // MLabNS returns at least one ip address + if (hostnameList.isEmpty()) + return IP_TYPE_CANNOT_DECIDE; + // Use the first result in the element + String hostname = hostnameList.get(0); + SocketAddress remoteAddr = new InetSocketAddress(hostname, portNum); + tcpSocket.setTcpNoDelay(true); + tcpSocket.connect(remoteAddr, tcpTimeout); + } catch (ConnectException e) { + // Server is not reachable due to client not support ipv6 + Logger.e("Connection exception is " + e.getMessage()); + return IP_TYPE_UNCONNECTIVITY; + } catch (IOException e) { + // Client timer expired + Logger.e("Fail to setup TCP in checkIPCompatibility(). " + + e.getMessage()); + return IP_TYPE_CANNOT_DECIDE; + } catch (InvalidParameterException e) { + // MLabNS service lookup fail + Logger.e("InvalidParameterException in checkIPCompatibility(). " + + e.getMessage()); + return IP_TYPE_CANNOT_DECIDE; + } catch (IllegalArgumentException e) { + Logger.e("IllegalArgumentException in checkIPCompatibility(). " + + e.getMessage()); + return IP_TYPE_CANNOT_DECIDE; + } finally { + try { + tcpSocket.close(); + } catch (IOException e) { + Logger.e("Fail to close TCP in checkIPCompatibility()."); + return IP_TYPE_CANNOT_DECIDE; + } + } + return IP_TYPE_CONNECTIVITY; + } + + /** + * Use MLabNS slices to check IPv4/IPv6 domain name resolvable + * @param ip_detect_type -- "ipv4" or "ipv6" + * @return DN_UNRESOLVABLE, DN_RESOLVABLE + */ + private int checkDomainNameResolvable(String ip_detect_type) { + if (!ip_detect_type.equals("ipv4") && !ip_detect_type.equals("ipv6")) { + return DN_UNKNOWN; + } + try { + ArrayList ipAddressList = MLabNS.Lookup(context, "mobiperf", + ip_detect_type, "fqdn"); + String ipAddress; + // MLabNS returns one fqdn each time + if (ipAddressList.size() == 1) { + ipAddress = ipAddressList.get(0); + } else { + return DN_UNKNOWN; + } + InetAddress inet = InetAddress.getByName(ipAddress); + if (inet != null) + return DN_RESOLVABLE; + } catch (UnknownHostException e) { + // Fail to resolve domain name + Logger.e("UnknownHostException in checkDomainNameResolvable() " + + e.getMessage()); + return DN_UNRESOLVABLE; + } catch (InvalidParameterException e) { + // MLabNS service lookup fail + Logger.e("InvalidParameterException in checkIPCompatibility(). " + + e.getMessage()); + return DN_UNRESOLVABLE; + } catch ( Exception e ) { + // "catch-all" + Logger.e("Unexpected Exception: " + e.getMessage()); + return DN_UNRESOLVABLE; + } + return DN_UNKNOWN; + } + + /** + * Summarize ip connectable cases + * @return ipv4, ipv6, ipv4_ipv6, IP_TYPE_NONE or IP_TYPE_UNKNOWN + */ + public String getIpConnectivity() { + int v4Conn = checkIPCompatibility("ipv4"); + int v6Conn = checkIPCompatibility("ipv6"); + if (v4Conn == IP_TYPE_CONNECTIVITY && v6Conn == IP_TYPE_CONNECTIVITY) + return IP_TYPE_IPV4_IPV6_BOTH; + if (v4Conn == IP_TYPE_CONNECTIVITY && v6Conn != IP_TYPE_CONNECTIVITY) + return IP_TYPE_IPV4_ONLY; + if (v4Conn != IP_TYPE_CONNECTIVITY && v6Conn == IP_TYPE_CONNECTIVITY) + return IP_TYPE_IPV6_ONLY; + if (v4Conn == IP_TYPE_UNCONNECTIVITY && v6Conn == IP_TYPE_UNCONNECTIVITY) + return IP_TYPE_NONE; + return IP_TYPE_UNKNOWN; + } + + /** + * Summarize Domain Name resolvability cases + * @return ipv4, ipv6, ipv4_ipv6, IP_TYPE_NONE or IP_TYPE_UNKNOWN + */ + public String getDnResolvability() { + int v4Resv = checkDomainNameResolvable("ipv4"); + int v6Resv = checkDomainNameResolvable("ipv6"); + if (v4Resv == DN_RESOLVABLE && v6Resv == DN_RESOLVABLE) + return IP_TYPE_IPV4_IPV6_BOTH; + if (v4Resv == DN_RESOLVABLE && v6Resv != DN_RESOLVABLE) + return IP_TYPE_IPV4_ONLY; + if (v4Resv != DN_RESOLVABLE && v6Resv == DN_RESOLVABLE) + return IP_TYPE_IPV6_ONLY; + if (v4Resv == DN_UNRESOLVABLE && v6Resv == DN_UNRESOLVABLE) + return IP_TYPE_NONE; + return IP_TYPE_UNKNOWN; + } + + /** Returns the DeviceProperty needed to report the measurement result */ + public DeviceProperty getDeviceProperty(String requestApp) { + String carrierName = telephonyManager.getNetworkOperatorName(); + Location location; + if (isTestingServer(getServerUrl())) { + location = getMockLocation(); + } else { + location = getLocation(); + } + + String networkCountryIso = telephonyManager.getNetworkCountryIso(); + +// NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); + String networkType = PhoneUtils.getPhoneUtils().getNetwork(); + String ipConnectivity = "NOT SUPPORTED"; + String dnResolvability = "NOT SUPPORTED"; + Logger.w("IP connectivity is " + ipConnectivity); + Logger.w("DN resolvability is " + dnResolvability); +// if (activeNetwork != null) { +// networkType = activeNetwork.getTypeName(); +// } + String versionName = PhoneUtils.getPhoneUtils().getAppVersionName(); + PhoneUtils utils = PhoneUtils.getPhoneUtils(); + + Logger.e("Request App is " + requestApp); + Logger.e("Host apps:"); + for ( String app : PhoneUtils.clientKeySet ) { + Logger.e(app); + } + String mobilyzerVersion = context.getString(R.string.scheduler_version_name); + Logger.i("Scheduler version = " + mobilyzerVersion); + return new DeviceProperty(getDeviceInfo().deviceId, versionName, + System.currentTimeMillis() * 1000, getVersionStr(), ipConnectivity, + dnResolvability, location.getLongitude(), location.getLatitude(), + location.getProvider(), networkType, carrierName, networkCountryIso, + utils.getCurrentBatteryLevel(), utils.isCharging(), + utils.getCellInfo(false), getCellRssi(), getWifiRSSI(), getWifiSSID(), getWifiBSSID(), getWifiIpAddress(), + mobilyzerVersion, PhoneUtils.clientKeySet, requestApp); + } +} diff --git a/src/com/mobilyzer/util/Util.java b/src/com/mobilyzer/util/Util.java new file mode 100755 index 0000000..6960273 --- /dev/null +++ b/src/com/mobilyzer/util/Util.java @@ -0,0 +1,237 @@ +/* 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.util; + + + + +import android.content.Context; +import android.net.TrafficStats; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.mobilyzer.Config; + + + +/** + * Utility class for Speedometer that does not require runtime information + */ +public class Util { + + /** + * Filter out values equal or beyond the bounds and then compute the average + * of valid data + * @param vals the list of values to be filtered + * @param lowerBound the lower bound of valid data + * @param upperBound the upper bound of valid data + * @return a list of filtered data within the specified bounds + */ + public static ArrayList applyInnerBandFilter(ArrayList vals, + double lowerBound, double upperBound) throws InvalidParameterException { + + double rrtTotal = 0; + int initResultLen = vals.size(); + if (initResultLen == 0) { + // Return the original array if it is of zero length. + throw new InvalidParameterException("The array size passed in is zero"); + } + + double rrtAvg = rrtTotal / initResultLen; + // we should filter out the outliers in the rrt result based on the average + ArrayList finalRrtResults = new ArrayList(); + int finalResultCnt = 0; + rrtTotal = 0; + for (double rrtVal : vals) { + if (rrtVal <= upperBound && rrtVal >= lowerBound) { + finalRrtResults.add(rrtVal); + } + } + + return finalRrtResults; + } + + /** + * Compute the sum of the values in list + * @param vals the list of values to sum up + * @return the sum of the values in the list + */ + public static double getSum(ArrayList vals) { + double sum = 0; + for (double val : vals) { + sum += val; + } + return sum; + } + + public static String constructCommand(Object... strings) + throws InvalidParameterException { + String finalCommand = ""; + int len = strings.length; + if (len < 0) { + throw new InvalidParameterException("0 arguments passed in for " + + "constructing command"); + } + + for (int i = 0; i < len - 1; i++) { + finalCommand += (strings[i] + " "); + } + finalCommand += strings[len - 1]; + return finalCommand; + } + + /** + * Prepare the internal User-Agent string for use. This requires a + * {@link Context} to pull the package name and version number for this + * application. + */ + public static String prepareUserAgent() { + return Config.USER_AGENT; + + } + + public static double getStandardDeviation(ArrayList values, double avg) { + double total = 0; + for (double val : values) { + double dev = val - avg; + total += (dev * dev); + } + if (total > 0) { + return Math.sqrt(total / values.size()); + } else { + return 0; + } + } + + public static String getTimeStringFromMicrosecond(long microsecond) { + Date timestamp = new Date(microsecond / 1000); + return timestamp.toString(); + } + + /** + * Returns a String array that contains the ICMP sequence number and the round + * trip time extracted from a ping output. The first array element is the + * sequence number and the second element is the round trip time. + * + * Returns a null object if either element cannot be found. + */ + public static String[] extractInfoFromPingOutput(String outputLine) { + try { + Pattern pattern = Pattern.compile("icmp_seq=([0-9]+)\\s.* time=([0-9]+(\\.[0-9]+)?)"); + Matcher matcher = pattern.matcher(outputLine); + matcher.find(); + + return new String[] {matcher.group(1), matcher.group(2)}; + } catch (IllegalStateException e) { + return null; + } + } + + /** + * Returns an integer array that contains the number of ICMP requests sent and the number + * of responses received. The first element is the requests sent and the second element + * is the responses received. + * + * Returns a null object if either element cannot be found. + */ + public static int[] extractPacketLossInfoFromPingOutput(String outputLine) { + try { + Pattern pattern = Pattern.compile("([0-9]+)\\spackets.*\\s([0-9]+)\\sreceived"); + Matcher matcher = pattern.matcher(outputLine); + matcher.find(); + + return new int[] {Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2))}; + } catch (IllegalStateException e) { + return null; + } catch (NumberFormatException e) { + return null; + } catch (NullPointerException e) { + return null; + } + } + + /** + * Return a list of system environment path + */ + public static String[] fetchEnvPaths() { + String path = ""; + Map env = System.getenv(); + if (env.containsKey("PATH")) { + path = env.get("PATH"); + } + return (path.contains(":")) ? path.split(":") : (new String[]{path}); + } + + /** + * Determine the ping executable based on ip address byte length + */ + public static String pingExecutableBasedOnIPType (int ipByteLen) { + Process testPingProc = null; + String[] progList = fetchEnvPaths(); + String pingExecutable = null; + if (progList != null && progList.length != 0) { + for (String pingLocation : progList) { + try { + if (ipByteLen == 4) { + pingExecutable = pingLocation + "/" + Config.PING_EXECUTABLE; +// context.getString(R.string.ping_executable); + } else if (ipByteLen == 16) { + pingExecutable = pingLocation + "/" + Config.PING6_EXECUTABLE; +// context.getString(R.string.ping6_executable); + } + testPingProc = Runtime.getRuntime().exec(pingExecutable); + } catch (IOException e) { + // reset the executable + pingExecutable = null; + // The ping command doesn't exist in that path, try another one + continue; + } finally { + if (testPingProc != null) + testPingProc.destroy(); + } + break; + } + } + return pingExecutable; + } + + /** + * Generates a random string (10 chars) that can be used as client key + * @return 10 least significant bytes of a random UUID + */ + public static String generateRandomClientKey(){ + UUID uuid=UUID.randomUUID(); + String uuidStr=uuid.toString(); + return uuidStr.substring(uuidStr.length()-10); + } + + + public static long getCurrentRxTxBytes(){ + int uid = android.os.Process.myUid(); + return TrafficStats.getUidRxBytes(uid)+TrafficStats.getUidTxBytes(uid); + } +} diff --git a/src/com/mobilyzer/util/video/EventLogger.java b/src/com/mobilyzer/util/video/EventLogger.java new file mode 100644 index 0000000..2f6b6b8 --- /dev/null +++ b/src/com/mobilyzer/util/video/EventLogger.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video; + +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.util.video.player.DemoPlayer; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.util.VerboseLogUtil; + +import android.content.Intent; +import android.media.MediaCodec.CryptoException; +import android.os.SystemClock; +import android.util.Log; +import android.util.Pair; + +import java.io.IOException; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; + +/** + * Logs player events using {@link Log}. + */ +public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener, + DemoPlayer.InternalErrorListener { + + public static HashMap, Integer> Resolution2Bitrate = new HashMap, Integer>(); + public static HashMap Id2Bitrate = new HashMap(); + + private ArrayList dropFrameTime; + private ArrayList> videoBitrateVarience; + private ArrayList> audioBitrateVarience; + private double initialLoadingTime; + private ArrayList rebufferTime; + private ArrayList> videoGoodput; + private ArrayList videoGoodputEstimate; + private ArrayList> audioGoodput; + private int previousVideoBitrate; + private int previousAudioBitrate; + private long switchToSteadyStateTime; + private long totalBytesDownloaded; + + private double initialLoadingTime_s; + private int bufferCounter = 0; + private double bufferTime_s; + + private static final String TAG = "EventLogger"; + private static final NumberFormat TIME_FORMAT; + static { + TIME_FORMAT = NumberFormat.getInstance(Locale.US); + TIME_FORMAT.setMinimumFractionDigits(4); + TIME_FORMAT.setMaximumFractionDigits(4); + } + + private long sessionStartTimeMs; + private long[] loadStartTimeMs; + + public EventLogger() { + loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; + + this.dropFrameTime = new ArrayList(); + this.videoBitrateVarience = new ArrayList>(); + this.audioBitrateVarience = new ArrayList>(); + this.rebufferTime = new ArrayList(); + this.videoGoodput = new ArrayList>(); + this.videoGoodputEstimate = new ArrayList(); + this.audioGoodput = new ArrayList>(); + this.videoBitrateVarience.add(Pair.create("0.00", 0)); + this.previousVideoBitrate = 0; + this.audioBitrateVarience.add(Pair.create("0.00", 0)); + this.previousAudioBitrate = 0; + this.switchToSteadyStateTime = -1; + this.totalBytesDownloaded = 0; + } + + public void startSession() { + sessionStartTimeMs = SystemClock.elapsedRealtime(); + Log.d(TAG, "start [0]"); + } + + public Intent endSession() { + Log.d(TAG, "end [" + getSessionTimeString() + "]"); + this.videoBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousVideoBitrate)); + this.audioBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousAudioBitrate)); + return printStatInfo(); + } + + // DemoPlayer.Listener + + @Override + public void onStateChanged(boolean playWhenReady, int state) { + Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " + + getStateString(state) + "]"); +// DecimalFormat df = new DecimalFormat("#.##"); + switch(state) { + case ExoPlayer.STATE_PREPARING: +// this.bitrateVarience.add(Pair.create("0.00", currentBitrate)); + this.initialLoadingTime_s = Double.parseDouble(getSessionTimeString()); + break; + + case ExoPlayer.STATE_BUFFERING: + bufferCounter++; + bufferTime_s = Double.parseDouble(getSessionTimeString()); + break; + case ExoPlayer.STATE_READY: + if (bufferCounter == 1) { + this.initialLoadingTime = Double.parseDouble(String.format("%.2f", Double.parseDouble(getSessionTimeString()) - this.initialLoadingTime_s)); + } + else { + this.rebufferTime.add(Double.parseDouble(String.format("%.2f", Double.parseDouble(getSessionTimeString()) - bufferTime_s))); + } + break; + case ExoPlayer.STATE_ENDED: + this.videoBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousVideoBitrate)); + this.audioBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousAudioBitrate)); +// printStatInfo(); + break; + } + } + + private Intent printStatInfo() { +// Log.e("", "DropFrame #: " + this.dropFrameTime.size()); +// Log.e("", "Bitrate: " + displayBitrate(this.bitrateVarience)); +// Log.e("", "Initial Loading Time: " + this.initialLoadingTime); +// Log.e("", "Rebuffering: " + this.rebufferTime); + Log.e("ashkan_video", "" + this.dropFrameTime.size()); + Log.e("ashkan_video", "" + displayGoodPut(this.videoGoodput)); + Log.e("ashkan_video", "" + displayBitrate(this.videoBitrateVarience)); + Log.e("ashkan_video", "" + this.initialLoadingTime); + Log.e("ashkan_video", "" + this.rebufferTime); + Log.e("ashkan_video", "" + displayGoodPut(this.audioGoodput)); + Log.e("ashkan_video", "" + displayBitrate(this.audioBitrateVarience)); + Log.e("ashkan_video", "bba switch time " +this.switchToSteadyStateTime ); + Log.e("ashkan_video", "total bytes downloaded " +this.totalBytesDownloaded ); + + Intent videoQoEResult = new Intent(); + videoQoEResult.setAction(UpdateIntent.VIDEO_MEASUREMENT_ACTION); + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_NUM_FRAME_DROPPED, this.dropFrameTime.size()); + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_INITIAL_LOADING_TIME, this.initialLoadingTime); + if(this.switchToSteadyStateTime!=-1){ + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BBA_SWITCH_TIME, this.switchToSteadyStateTime); + } + if(this.totalBytesDownloaded!=0){ + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BYTE_USED, this.totalBytesDownloaded); + } + double[] rebufferTimeArray = new double[this.rebufferTime.size()]; + int counter = 0; + for (Double rebufferSample : this.rebufferTime) { + rebufferTimeArray[counter] = rebufferSample; + counter++; + } + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME, rebufferTimeArray); + String[] goodputTimestamp = new String[this.videoGoodput.size()]; + double[] goodputValue = new double[this.videoGoodput.size()]; + long[] goodputEstimate = new long[this.videoGoodputEstimate.size()]; + counter = 0; + for (Pair goodputSample : this.videoGoodput) { + goodputTimestamp[counter] = goodputSample.first; + goodputValue[counter] = goodputSample.second; + counter++; + } + + counter=0; + for (Long estimate : this.videoGoodputEstimate) { + goodputEstimate[counter] = estimate; + counter++; + } + + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP, goodputTimestamp); + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE, goodputValue); + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_ESTIMATE_VALUE, goodputEstimate); + + String[] bitrateTimestamp = new String[this.videoBitrateVarience.size()]; + int[] bitrateValue = new int[this.videoBitrateVarience.size()]; + counter=0; + for (Pair bitrateSample : this.videoBitrateVarience) { + bitrateTimestamp[counter] = bitrateSample.first; + bitrateValue[counter] = bitrateSample.second; + counter++; + } + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP, bitrateTimestamp); + videoQoEResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE, bitrateValue); + + return videoQoEResult; + } + + + private String displayBitrate(ArrayList> bitrateVarience2) { + StringBuilder sBuilder = new StringBuilder(); + for (Pair pBitrate : bitrateVarience2) { + sBuilder.append(pBitrate.first + " " + pBitrate.second + "\n"); + } + return sBuilder.toString(); + } + + private String displayGoodPut(ArrayList> goodput) { + StringBuilder sBuilder = new StringBuilder(); + for (Pair pBitrate : goodput) { + sBuilder.append(pBitrate.first + " " + String.format("%.2f", pBitrate.second) + "\n"); + } + return sBuilder.toString(); + } + + // Error finished, return partial results + @Override + public void onError(Exception e) { + Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); +// this.videoBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousVideoBitrate)); +// this.audioBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousAudioBitrate)); +// printStatInfo(); + } + + @Override + public void onVideoSizeChanged(int width, int height) { +// currentBitrate = Resolution2Bitrate.get(Pair.create(width, height)); + Log.d(TAG, "videoSizeChanged [" + getSessionTimeString() + ", " + width + ", " + height + "]"); +// this.bitrateVarience.add(Pair.create(getSessionTimeString(), currentBitrate)); + } + + // DemoPlayer.InfoListener + + @Override +// public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { + public void onBandwidthSample(String label, long startTime, long endTime, int elapsedMs, long bytes, long bitrateEstimate) { + Log.d(TAG, label + " bandwidth [" + startTime + ", " + endTime + ", " + getSessionTimeString() + ", " + bytes + + ", " + getTimeString(elapsedMs) + ", " + bitrateEstimate + ", " + bytes * 8 / elapsedMs + "kbps]"); + if (label.equals("video")) { + this.videoGoodput.add(Pair.create(getSessionTimeString(), (double)bytes * 8000 / elapsedMs)); + this.videoGoodputEstimate.add(bitrateEstimate); + } + else if (label.equals("audio")) { + this.audioGoodput.add(Pair.create(getSessionTimeString(), (double)bytes * 8000 / elapsedMs)); + } +// this.goodput.add(Pair.create(getSessionTimeString(), (double)bytes * 8000 / elapsedMs)); + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + this.dropFrameTime.add(getSessionTimeString()); + } + + @Override + public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long length) { + loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); + if (VerboseLogUtil.isTagEnabled(TAG)) { + Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); + } + } + + @Override + public void onLoadCompleted(int sourceId, long bytesLoaded) { + if (VerboseLogUtil.isTagEnabled(TAG)) { + long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; + Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + + downloadTime + "]"); + } + } + + @Override + public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) { + int currentBitrate = Id2Bitrate.get(formatId); + Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " + + Integer.toString(trigger) + ", " + currentBitrate / 1000 + "kbps" + "]"); + this.videoBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousVideoBitrate)); + this.videoBitrateVarience.add(Pair.create(getSessionTimeString(), currentBitrate)); + this.previousVideoBitrate = currentBitrate; + } + + @Override + public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) { + int currentBitrate = Id2Bitrate.get(formatId); + Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " + + Integer.toString(trigger) + ", " + currentBitrate / 1000 + "kbps" + "]"); + this.audioBitrateVarience.add(Pair.create(getSessionTimeString(), this.previousAudioBitrate)); + this.audioBitrateVarience.add(Pair.create(getSessionTimeString(), currentBitrate)); + this.previousAudioBitrate = currentBitrate; + } + + // DemoPlayer.InternalErrorListener + + @Override + public void onUpstreamError(int sourceId, IOException e) { + printInternalError("upstreamError", e); + } + + @Override + public void onConsumptionError(int sourceId, IOException e) { + printInternalError("consumptionError", e); + } + + @Override + public void onRendererInitializationError(Exception e) { + printInternalError("rendererInitError", e); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + printInternalError("drmSessionManagerError", e); + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + printInternalError("decoderInitializationError", e); + } + + @Override + public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + printInternalError("audioTrackInitializationError", e); + } + + @Override + public void onCryptoError(CryptoException e) { + printInternalError("cryptoError", e); + } + + private void printInternalError(String type, Exception e) { + Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); + } + + private String getStateString(int state) { + switch (state) { + case ExoPlayer.STATE_BUFFERING: + return "B"; + case ExoPlayer.STATE_ENDED: + return "E"; + case ExoPlayer.STATE_IDLE: + return "I"; + case ExoPlayer.STATE_PREPARING: + return "P"; + case ExoPlayer.STATE_READY: + return "R"; + default: + return "?"; + } + } + + private String getSessionTimeString() { + return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); +// return getTimeString(System.currentTimeMillis()); + } + + private String getTimeString(long timeMs) { + return TIME_FORMAT.format((timeMs) / 1000f); + } + + @Override + public void onSwitchToSteadyState(long elapsedMs) { + this.switchToSteadyStateTime=elapsedMs; + } + + @Override + public void onAllChunksDownloaded(long totalBytes) { + this.totalBytesDownloaded=totalBytes; + } + +} diff --git a/src/com/mobilyzer/util/video/VideoPlayerService.java b/src/com/mobilyzer/util/video/VideoPlayerService.java new file mode 100644 index 0000000..8e4a8d5 --- /dev/null +++ b/src/com/mobilyzer/util/video/VideoPlayerService.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video; + +import com.mobilyzer.UpdateIntent; +import com.mobilyzer.util.Logger; +import com.mobilyzer.util.video.player.DashVodRendererBuilder; +import com.mobilyzer.util.video.player.DefaultRendererBuilder; +import com.mobilyzer.util.video.player.DemoPlayer; +import com.mobilyzer.util.video.player.DashVodRendererBuilder.AdaptiveType; +import com.mobilyzer.util.video.player.DemoPlayer.RendererBuilder; +import com.mobilyzer.util.video.util.DemoUtil; +import com.google.android.exoplayer.ExoPlayer; + +import android.app.Service; +import android.content.Intent; +import android.graphics.SurfaceTexture; +import android.net.Uri; +import android.opengl.GLES20; +import android.os.IBinder; +import android.util.Log; +import android.view.Surface; + +/** + * An activity that plays media using {@link DemoPlayer}. + */ +public class VideoPlayerService extends Service implements //SurfaceHolder.Callback, + DemoPlayer.Listener { + + private static final int MENU_GROUP_TRACKS = 1; + private static final int ID_OFFSET = 2; + + private EventLogger eventLogger; +// private TextView debugTextView; +// private TextView playerStateTextView; + + private DemoPlayer player; + private boolean playerNeedsPrepare; + + private boolean autoPlay = true; + private int playerPosition; + private boolean enableBackgroundAudio = false; + + private Uri contentUri; + private int contentType; + private String contentId; + + private SurfaceTexture mTexture; + private Surface mSurface; + + private boolean isResultSent; + @Override + public int onStartCommand(Intent intent, int flags, int startId){ + Logger.i("Video Player service started!"); + contentUri = intent.getData(); + contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, DemoUtil.TYPE_PROGRESSIVE); + contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA); + + this.isResultSent = false; + preparePlayer(); + + return START_NOT_STICKY; + + } + + @Override + public void onDestroy() { + super.onDestroy(); + releasePlayer(true); + } + + + // Internal methods + + private RendererBuilder getRendererBuilder() { + String userAgent = DemoUtil.getUserAgent(this); + if (this.contentType == DemoUtil.TYPE_DASH_VOD) { + return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, + new WidevineTestMediaDrmCallback(contentId), null, AdaptiveType.CBA ); + } + else if (this.contentType == DemoUtil.TYPE_BBA){ + return new DashVodRendererBuilder(userAgent, contentUri.toString(), contentId, + new WidevineTestMediaDrmCallback(contentId), null, AdaptiveType.BBA); + } + else if (this.contentType == DemoUtil.TYPE_PROGRESSIVE) { + return new DefaultRendererBuilder(this, contentUri, null); + } + else { + return null; + } + } + + private void preparePlayer() { + if (player == null) { + player = new DemoPlayer(getRendererBuilder()); + player.addListener(this); + player.seekTo(playerPosition); + playerNeedsPrepare = true; + eventLogger = new EventLogger(); + eventLogger.startSession(); + player.addListener(eventLogger); + player.setInfoListener(eventLogger); + player.setInternalErrorListener(eventLogger); + } + if (playerNeedsPrepare) { + player.prepare(); + playerNeedsPrepare = false; + } + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + int textureID = textures[0]; + Log.e("TextureId", "" + textureID); + mTexture = new SurfaceTexture(textureID); + mSurface = new Surface(mTexture); + player.setSurface(mSurface); + maybeStartPlayback(); + } + + private void maybeStartPlayback() { + if (autoPlay) { + player.setPlayWhenReady(true); + autoPlay = false; + } + } + + private void releasePlayer(boolean isSucceed) { + if (player != null) { + playerPosition = player.getCurrentPosition(); + player.release(); + player = null; + if (!isResultSent) { + Intent videoResult = eventLogger.endSession(); + videoResult.putExtra(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED, isSucceed); + this.sendBroadcast(videoResult); + isResultSent = true; + } + eventLogger = null; + } + } + + // DemoPlayer.Listener implementation + + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + String text = "playWhenReady=" + playWhenReady + ", playbackState="; + switch(playbackState) { + case ExoPlayer.STATE_BUFFERING: + text += "buffering"; + break; + case ExoPlayer.STATE_ENDED: + text += "ended"; + break; + case ExoPlayer.STATE_IDLE: + text += "idle"; + break; + case ExoPlayer.STATE_PREPARING: + text += "preparing"; + break; + case ExoPlayer.STATE_READY: + text += "ready"; + break; + default: + text += "unknown"; + break; + } + Log.e("", text); + + if (playbackState == ExoPlayer.STATE_ENDED) { + Log.e("", "Playback ended!"); + releasePlayer(true); + this.stopSelf(); + } +// playerStateTextView.setText(text); + } + + @Override + public void onError(Exception e) { + Log.e("BgPlayerService", "Error occurs!"); + playerNeedsPrepare = true; + releasePlayer(false); + this.stopSelf(); + } + + @Override + public void onVideoSizeChanged(int width, int height) { + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + +} diff --git a/src/com/mobilyzer/util/video/WidevineTestMediaDrmCallback.java b/src/com/mobilyzer/util/video/WidevineTestMediaDrmCallback.java new file mode 100644 index 0000000..bfae22b --- /dev/null +++ b/src/com/mobilyzer/util/video/WidevineTestMediaDrmCallback.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video; + +import com.mobilyzer.util.video.util.DemoUtil; +import com.google.android.exoplayer.drm.MediaDrmCallback; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import org.apache.http.client.ClientProtocolException; + +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} for Widevine test content. + */ +@TargetApi(18) +public class WidevineTestMediaDrmCallback implements MediaDrmCallback { + + private static final String WIDEVINE_GTS_DEFAULT_BASE_URI = + "http://wv-staging-proxy.appspot.com/proxy?provider=YouTube&video_id="; + + private final String defaultUri; + + public WidevineTestMediaDrmCallback(String videoId) { + defaultUri = WIDEVINE_GTS_DEFAULT_BASE_URI + videoId; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) + throws ClientProtocolException, IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return DemoUtil.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws IOException { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = defaultUri; + } + return DemoUtil.executePost(url, request.getData(), null); + } + +} diff --git a/src/com/mobilyzer/util/video/player/DashVodRendererBuilder.java b/src/com/mobilyzer/util/video/player/DashVodRendererBuilder.java new file mode 100644 index 0000000..49d5710 --- /dev/null +++ b/src/com/mobilyzer/util/video/player/DashVodRendererBuilder.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video.player; + +import com.mobilyzer.util.video.EventLogger; +import com.mobilyzer.util.video.player.DemoPlayer.RendererBuilder; +import com.mobilyzer.util.video.player.DemoPlayer.RendererBuilderCallback; +import com.mobilyzer.util.video.util.DemoUtil; +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.ChunkSource; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.chunk.FormatEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.BufferBasedAdaptiveEvaluator; +import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.Representation; +import com.google.android.exoplayer.drm.DrmSessionManager; +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.HttpDataSource; +import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.UnsupportedSchemeException; +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Pair; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A {@link RendererBuilder} for DASH VOD. + */ +public class DashVodRendererBuilder implements RendererBuilder, + ManifestCallback { + + private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; +// private static final int VIDEO_BUFFER_SEGMENTS = 200; + private static final int VIDEO_BUFFER_SEGMENTS = 1000; + private static final int AUDIO_BUFFER_SEGMENTS = 60; + + private static final int SECURITY_LEVEL_UNKNOWN = -1; + private static final int SECURITY_LEVEL_1 = 1; + private static final int SECURITY_LEVEL_3 = 3; + + + public enum AdaptiveType{ + BBA, CBA + }; + + private final String userAgent; + private final String url; + private final String contentId; + private final MediaDrmCallback drmCallback; + private final TextView debugTextView; + + private DemoPlayer player; + private RendererBuilderCallback callback; + + private AdaptiveType adaptiveType; + + public DashVodRendererBuilder(String userAgent, String url, String contentId, + MediaDrmCallback drmCallback, TextView debugTextView, AdaptiveType adaptiveType) { + this.userAgent = userAgent; + this.url = url; + this.contentId = contentId; + this.drmCallback = drmCallback; + this.debugTextView = debugTextView; + this.adaptiveType=adaptiveType; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + this.player = player; + this.callback = callback; + MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this); + mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + } + + @Override + public void onManifestError(String contentId, Exception e) { + callback.onRenderersError(e); + } + + @Override + public void onManifest(String contentId, MediaPresentationDescription manifest) { + Handler mainHandler = player.getMainHandler(); + LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); +// DefaultBandwidthMeter videoBandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); + DefaultBandwidthMeter videoBandwidthMeter = new DefaultBandwidthMeter("video", mainHandler, player); + DefaultBandwidthMeter audioBandwidthMeter = new DefaultBandwidthMeter("audio", mainHandler, player); + + // Obtain Representations for playback. + int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); + ArrayList audioRepresentationsList = new ArrayList(); + ArrayList videoRepresentationsList = new ArrayList(); + Period period = manifest.periods.get(0); + boolean hasContentProtection = false; + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + hasContentProtection |= adaptationSet.hasContentProtection(); + int adaptationSetType = adaptationSet.type; + for (int j = 0; j < adaptationSet.representations.size(); j++) { + Representation representation = adaptationSet.representations.get(j); + if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { + audioRepresentationsList.add(representation); + } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { + Format format = representation.format; + if (format.width * format.height <= maxDecodableFrameSize) { + videoRepresentationsList.add(representation); + } else { + // The device isn't capable of playing this stream. + } + } + } + } + Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()]; + videoRepresentationsList.toArray(videoRepresentations); + + // Hongyi: create lookup table from video resolution and resource id to video bitrate + for(Representation r : videoRepresentations) { + EventLogger.Resolution2Bitrate.put(Pair.create(r.format.width, r.format.height), r.format.bitrate); + EventLogger.Id2Bitrate.put(r.format.id, r.format.bitrate); + } + // Hongyi: create lookup table from audio resource id to audio bitrate + for(Representation r : audioRepresentationsList) { + EventLogger.Id2Bitrate.put(r.format.id, r.format.bitrate); + } + + // Check drm support if necessary. + DrmSessionManager drmSessionManager = null; + if (hasContentProtection) { + if (Util.SDK_INT < 18) { + callback.onRenderersError(new UnsupportedOperationException( + "Protected content not supported on API level " + Util.SDK_INT)); + return; + } + try { + Pair drmSessionManagerData = + V18Compat.getDrmSessionManagerData(player, drmCallback); + drmSessionManager = drmSessionManagerData.first; + if (!drmSessionManagerData.second) { + // HD streams require L1 security. + videoRepresentations = getSdRepresentations(videoRepresentations); + } + } catch (Exception e) { + callback.onRenderersError(e); + return; + } + } + + // Build the video renderer. +// DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource videoDataSource = new HttpDataSource(userAgent, null, videoBandwidthMeter); + ChunkSource videoChunkSource; + String mimeType = videoRepresentations[0].format.mimeType; + if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) { +// videoChunkSource = new DashChunkSource(videoDataSource, +// new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); + if (adaptiveType==AdaptiveType.CBA){ + videoChunkSource = new DashChunkSource(videoDataSource, + new AdaptiveEvaluator(videoBandwidthMeter, manifest.duration,mainHandler, player), videoRepresentations); + }else{ + videoChunkSource = new DashChunkSource(videoDataSource, + new BufferBasedAdaptiveEvaluator(videoBandwidthMeter, manifest.duration, mainHandler, player ), videoRepresentations); + } + } else { + throw new IllegalStateException("Unexpected mime type: " + mimeType); + } + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_VIDEO); +// MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, +// drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, +// mainHandler, player, 50); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + mainHandler, player, 1); + + // Build the audio renderer. + final String[] audioTrackNames; + final MultiTrackChunkSource audioChunkSource; + final MediaCodecAudioTrackRenderer audioRenderer; + if (audioRepresentationsList.isEmpty()) { + audioTrackNames = null; + audioChunkSource = null; + audioRenderer = null; + } else { +// DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter); + DataSource audioDataSource = new HttpDataSource(userAgent, null, audioBandwidthMeter); + audioTrackNames = new String[audioRepresentationsList.size()]; + ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; + FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); + for (int i = 0; i < audioRepresentationsList.size(); i++) { + Representation representation = audioRepresentationsList.get(i); + Format format = representation.format; + audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " + + format.audioSamplingRate + "Hz)"; + audioChunkSources[i] = new DashChunkSource(audioDataSource, + audioEvaluator, representation); + } + audioChunkSource = new MultiTrackChunkSource(audioChunkSources); + SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, + DemoPlayer.TYPE_AUDIO); + audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, + mainHandler, player); + } + + // Build the debug renderer. + TrackRenderer debugRenderer = debugTextView != null + ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource) : null; + + // Invoke the callback. + String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; + trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; + + MultiTrackChunkSource[] multiTrackChunkSources = + new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; + multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; + + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; + callback.onRenderers(trackNames, multiTrackChunkSources, renderers); + } + + private Representation[] getSdRepresentations(Representation[] representations) { + ArrayList sdRepresentations = new ArrayList(); + for (int i = 0; i < representations.length; i++) { + if (representations[i].format.height < 720 && representations[i].format.width < 1280) { + sdRepresentations.add(representations[i]); + } + } + Representation[] sdRepresentationArray = new Representation[sdRepresentations.size()]; + sdRepresentations.toArray(sdRepresentationArray); + return sdRepresentationArray; + } + + @TargetApi(18) + private static class V18Compat { + + public static Pair getDrmSessionManagerData(DemoPlayer player, + MediaDrmCallback drmCallback) throws UnsupportedSchemeException { + StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( + DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, player.getMainHandler(), + player); + return Pair.create((DrmSessionManager) streamingDrmSessionManager, + getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); + } + + private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { + String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); + return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty + .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; + } + + } + +} diff --git a/src/com/mobilyzer/util/video/player/DebugTrackRenderer.java b/src/com/mobilyzer/util/video/player/DebugTrackRenderer.java new file mode 100644 index 0000000..a86df39 --- /dev/null +++ b/src/com/mobilyzer/util/video/player/DebugTrackRenderer.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video.player; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.Format; + +import android.widget.TextView; + +/** + * A {@link TrackRenderer} that periodically updates debugging information displayed by a + * {@link TextView}. + */ +/* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable { + + private final TextView textView; + private final MediaCodecTrackRenderer renderer; + private final ChunkSampleSource videoSampleSource; + + private volatile boolean pendingFailure; + private volatile long currentPositionUs; + + public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer) { + this(textView, renderer, null); + } + + public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer, + ChunkSampleSource videoSampleSource) { + this.textView = textView; + this.renderer = renderer; + this.videoSampleSource = videoSampleSource; + } + + public void injectFailure() { + pendingFailure = true; + } + + @Override + protected boolean isEnded() { + return true; + } + + @Override + protected boolean isReady() { + return true; + } + + @Override + protected int doPrepare() throws ExoPlaybackException { + maybeFail(); + return STATE_PREPARED; + } + + @Override + protected void doSomeWork(long timeUs) throws ExoPlaybackException { + maybeFail(); + if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) { + currentPositionUs = timeUs; + textView.post(this); + } + } + + @Override + public void run() { + textView.setText(getRenderString()); + } + + private String getRenderString() { + return "ms(" + (currentPositionUs / 1000) + "), " + getQualityString() + + ", " + renderer.codecCounters.getDebugString(); + } + + private String getQualityString() { + Format format = videoSampleSource == null ? null : videoSampleSource.getFormat(); + return format == null ? "null" : "height(" + format.height + "), itag(" + format.id + ")"; + } + + @Override + protected long getCurrentPositionUs() { + return currentPositionUs; + } + + @Override + protected long getDurationUs() { + return TrackRenderer.MATCH_LONGEST_US; + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK_US; + } + + @Override + protected void seekTo(long timeUs) { + currentPositionUs = timeUs; + } + + private void maybeFail() throws ExoPlaybackException { + if (pendingFailure) { + pendingFailure = false; + throw new ExoPlaybackException("fail() was called on DebugTrackRenderer"); + } + } + +} diff --git a/src/com/mobilyzer/util/video/player/DefaultRendererBuilder.java b/src/com/mobilyzer/util/video/player/DefaultRendererBuilder.java new file mode 100644 index 0000000..f3d26cc --- /dev/null +++ b/src/com/mobilyzer/util/video/player/DefaultRendererBuilder.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video.player; + +import com.google.android.exoplayer.FrameworkSampleSource; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.mobilyzer.util.video.player.DemoPlayer.RendererBuilder; +import com.mobilyzer.util.video.player.DemoPlayer.RendererBuilderCallback; + +import android.content.Context; +import android.media.MediaCodec; +import android.net.Uri; +import android.widget.TextView; + +/** + * A {@link RendererBuilder} for streams that can be read using + * {@link android.media.MediaExtractor}. + */ +public class DefaultRendererBuilder implements RendererBuilder { + + private final Context context; + private final Uri uri; + private final TextView debugTextView; + + public DefaultRendererBuilder(Context context, Uri uri, TextView debugTextView) { + this.context = context; + this.uri = uri; + this.debugTextView = debugTextView; + } + + @Override + public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { + // Build the video and audio renderers. + FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, + null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, + player.getMainHandler(), player, 1); + MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, + null, true, player.getMainHandler(), player); + + // Build the debug renderer. + TrackRenderer debugRenderer = debugTextView != null + ? new DebugTrackRenderer(debugTextView, videoRenderer) + : null; + + // Invoke the callback. + TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; + renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; + renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; + renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; + callback.onRenderers(null, null, renderers); + } + +} diff --git a/src/com/mobilyzer/util/video/player/DemoPlayer.java b/src/com/mobilyzer/util/video/player/DemoPlayer.java new file mode 100644 index 0000000..4cda43c --- /dev/null +++ b/src/com/mobilyzer/util/video/player/DemoPlayer.java @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video.player; + +import com.google.android.exoplayer.DummyTrackRenderer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException; +import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.chunk.ChunkSampleSource; +import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.text.TextTrackRenderer; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.util.PlayerControl; +import com.google.android.exoplayer.chunk.FormatEvaluator.BufferBasedAdaptiveEvaluator; +import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; + +import android.media.MediaCodec.CryptoException; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared + * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, + * SmoothStreaming and so on). + */ +public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, + DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, + MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, + StreamingDrmSessionManager.EventListener, BufferBasedAdaptiveEvaluator.EventListener, + AdaptiveEvaluator.EventListener{ + + /** + * Builds renderers for the player. + */ + public interface RendererBuilder { + /** + * Constructs the necessary components for playback. + * + * @param player The parent player. + * @param callback The callback to invoke with the constructed components. + */ + void buildRenderers(DemoPlayer player, RendererBuilderCallback callback); + } + + /** + * A callback invoked by a {@link RendererBuilder}. + */ + public interface RendererBuilderCallback { + /** + * Invoked with the results from a {@link RendererBuilder}. + * + * @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_* + * constants. May be null if the track names are unknown. An individual element may be null + * if the track names are unknown for the corresponding type. + * @param multiTrackSources Sources capable of switching between multiple available tracks, + * indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with + * multiple tracks. An individual element may be null if it does not have multiple tracks. + * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual + * element may be null if there do not exist tracks of the corresponding type. + */ + void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, + TrackRenderer[] renderers); + /** + * Invoked if a {@link RendererBuilder} encounters an error. + * + * @param e Describes the error. + */ + void onRenderersError(Exception e); + } + + /** + * A listener for core events. + */ + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + void onError(Exception e); + void onVideoSizeChanged(int width, int height); + } + + /** + * A listener for internal errors. + *

+ * These errors are not visible to the user, and hence this listener is provided for + * informational purposes only. Note however that an internal error may cause a fatal + * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)} + * will be invoked. + */ + public interface InternalErrorListener { + void onRendererInitializationError(Exception e); + void onAudioTrackInitializationError(AudioTrackInitializationException e); + void onDecoderInitializationError(DecoderInitializationException e); + void onCryptoError(CryptoException e); + void onUpstreamError(int sourceId, IOException e); + void onConsumptionError(int sourceId, IOException e); + void onDrmSessionManagerError(Exception e); + } + + /** + * A listener for debugging information. + */ + public interface InfoListener { + void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs); + void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); + void onDroppedFrames(int count, long elapsed); +// void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); + void onBandwidthSample(String label, long startTime, long endTime, int elapsedMs, long bytes, long bitrateEstimate); + void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long length); + void onLoadCompleted(int sourceId, long bytesLoaded); + void onSwitchToSteadyState(long elapsedMs); + void onAllChunksDownloaded(long totalBytes); + } + + /** + * A listener for receiving notifications of timed text. + */ + public interface TextListener { + public abstract void onText(String text); + } + + // Constants pulled into this class for convenience. + public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; + public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; + public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; + public static final int STATE_READY = ExoPlayer.STATE_READY; + public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; + + public static final int DISABLED_TRACK = -1; + public static final int PRIMARY_TRACK = 0; + + public static final int RENDERER_COUNT = 4; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_TEXT = 2; + public static final int TYPE_DEBUG = 3; + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private final RendererBuilder rendererBuilder; + private final ExoPlayer player; + private final PlayerControl playerControl; + private final Handler mainHandler; + private final CopyOnWriteArrayList listeners; + + private int rendererBuildingState; + private int lastReportedPlaybackState; + private boolean lastReportedPlayWhenReady; + + private Surface surface; + private InternalRendererBuilderCallback builderCallback; + private TrackRenderer videoRenderer; + + private MultiTrackChunkSource[] multiTrackSources; + private String[][] trackNames; + private int[] selectedTracks; + + private TextListener textListener; + private InternalErrorListener internalErrorListener; + private InfoListener infoListener; + + public DemoPlayer(RendererBuilder rendererBuilder) { + this.rendererBuilder = rendererBuilder; + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + playerControl = new PlayerControl(player); + mainHandler = new Handler(); + listeners = new CopyOnWriteArrayList(); + lastReportedPlaybackState = STATE_IDLE; + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + selectedTracks = new int[RENDERER_COUNT]; + // Disable text initially. + selectedTracks[TYPE_TEXT] = DISABLED_TRACK; + } + + public PlayerControl getPlayerControl() { + return playerControl; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public void setInternalErrorListener(InternalErrorListener listener) { + internalErrorListener = listener; + } + + public void setInfoListener(InfoListener listener) { + infoListener = listener; + } + + public void setTextListener(TextListener listener) { + textListener = listener; + } + + public void setSurface(Surface surface) { + this.surface = surface; + pushSurfaceAndVideoTrack(false); + } + + public Surface getSurface() { + return surface; + } + + public void blockingClearSurface() { + surface = null; + pushSurfaceAndVideoTrack(true); + } + + public String[] getTracks(int type) { + return trackNames == null ? null : trackNames[type]; + } + + public int getSelectedTrackIndex(int type) { + return selectedTracks[type]; + } + + public void selectTrack(int type, int index) { + if (selectedTracks[type] == index) { + return; + } + selectedTracks[type] = index; + if (type == TYPE_VIDEO) { + pushSurfaceAndVideoTrack(false); + } else { + pushTrackSelection(type, true); + } + } + + public void prepare() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + player.stop(); + } + if (builderCallback != null) { + builderCallback.cancel(); + } + rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + maybeReportPlayerState(); + builderCallback = new InternalRendererBuilderCallback(); + rendererBuilder.buildRenderers(this, builderCallback); + } + + /* package */ void onRenderers(String[][] trackNames, + MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) { + builderCallback = null; + // Normalize the results. + if (trackNames == null) { + trackNames = new String[RENDERER_COUNT][]; + } + if (multiTrackSources == null) { + multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT]; + } + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + // Convert a null renderer to a dummy renderer. + renderers[i] = new DummyTrackRenderer(); + } else if (trackNames[i] == null) { + // We have a renderer so we must have at least one track, but the names are unknown. + // Initialize the correct number of null track names. + int trackCount = multiTrackSources[i] == null ? 1 : multiTrackSources[i].getTrackCount(); + trackNames[i] = new String[trackCount]; + } + } + // Complete preparation. + this.videoRenderer = renderers[TYPE_VIDEO]; + this.trackNames = trackNames; + this.multiTrackSources = multiTrackSources; + rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + maybeReportPlayerState(); + pushSurfaceAndVideoTrack(false); + pushTrackSelection(TYPE_AUDIO, true); + pushTrackSelection(TYPE_TEXT, true); + player.prepare(renderers); + // silent the player + player.sendMessage(renderers[TYPE_AUDIO], MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, (float) 0.0); + } + + /* package */ void onRenderersError(Exception e) { + builderCallback = null; + if (internalErrorListener != null) { + internalErrorListener.onRendererInitializationError(e); + } + for (Listener listener : listeners) { + listener.onError(e); + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + maybeReportPlayerState(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + } + + public void seekTo(int positionMs) { + player.seekTo(positionMs); + } + + public void release() { + if (builderCallback != null) { + builderCallback.cancel(); + builderCallback = null; + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + surface = null; + player.release(); + } + + + public int getPlaybackState() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return ExoPlayer.STATE_PREPARING; + } + int playerState = player.getPlaybackState(); + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT + && rendererBuildingState == RENDERER_BUILDING_STATE_IDLE) { + // This is an edge case where the renderers are built, but are still being passed to the + // player's playback thread. + return ExoPlayer.STATE_PREPARING; + } + return playerState; + } + + public int getCurrentPosition() { + return player.getCurrentPosition(); + } + + public int getDuration() { + return player.getDuration(); + } + + public int getBufferedPercentage() { + return player.getBufferedPercentage(); + } + + public boolean getPlayWhenReady() { + return player.getPlayWhenReady(); + } + + /* package */ Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + /* package */ Handler getMainHandler() { + return mainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + maybeReportPlayerState(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + for (Listener listener : listeners) { + listener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged(int width, int height) { + for (Listener listener : listeners) { + listener.onVideoSizeChanged(width, height); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + if (infoListener != null) { + infoListener.onDroppedFrames(count, elapsed); + } + } + + @Override +// public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { + public void onBandwidthSample(String label, long startTime, long endTime, int elapsedMs, long bytes, long bitrateEstimate) { + if (infoListener != null) { + infoListener.onBandwidthSample(label, startTime, endTime, elapsedMs, bytes, bitrateEstimate); + } + } + + @Override + public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, + int mediaTimeMs) { + if (infoListener == null) { + return; + } + if (sourceId == TYPE_VIDEO) { + infoListener.onVideoFormatEnabled(formatId, trigger, mediaTimeMs); + } else if (sourceId == TYPE_AUDIO) { + infoListener.onAudioFormatEnabled(formatId, trigger, mediaTimeMs); + } + } + + @Override + public void onDrmSessionManagerError(Exception e) { + if (internalErrorListener != null) { + internalErrorListener.onDrmSessionManagerError(e); + } + } + + @Override + public void onDecoderInitializationError(DecoderInitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onDecoderInitializationError(e); + } + } + + @Override + public void onAudioTrackInitializationError(AudioTrackInitializationException e) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackInitializationError(e); + } + } + + @Override + public void onCryptoError(CryptoException e) { + if (internalErrorListener != null) { + internalErrorListener.onCryptoError(e); + } + } + + @Override + public void onUpstreamError(int sourceId, IOException e) { + if (internalErrorListener != null) { + internalErrorListener.onUpstreamError(sourceId, e); + } + } + + @Override + public void onConsumptionError(int sourceId, IOException e) { + if (internalErrorListener != null) { + internalErrorListener.onConsumptionError(sourceId, e); + } + } + + @Override + public void onText(String text) { + if (textListener != null) { + textListener.onText(text); + } + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + // Do nothing. + } + + @Override + public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, + int mediaStartTimeMs, int mediaEndTimeMs, long length) { + if (infoListener != null) { + infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, + mediaEndTimeMs, length); + } + } + + @Override + public void onLoadCompleted(int sourceId, long bytesLoaded) { + if (infoListener != null) { + infoListener.onLoadCompleted(sourceId, bytesLoaded); + } + } + + @Override + public void onLoadCanceled(int sourceId, long bytesLoaded) { + // Do nothing. + } + + @Override + public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, + long bytesDiscarded) { + // Do nothing. + } + + @Override + public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, + long bytesDiscarded) { + // Do nothing. + } + + private void maybeReportPlayerState() { + boolean playWhenReady = player.getPlayWhenReady(); + int playbackState = getPlaybackState(); + if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { + for (Listener listener : listeners) { + listener.onStateChanged(playWhenReady, playbackState); + } + lastReportedPlayWhenReady = playWhenReady; + lastReportedPlaybackState = playbackState; + } + } + + private void pushSurfaceAndVideoTrack(boolean blockForSurfacePush) { + if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + + if (blockForSurfacePush) { + player.blockingSendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } else { + player.sendMessage( + videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } + pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid()); + } + + private void pushTrackSelection(int type, boolean allowRendererEnable) { + if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { + return; + } + + int trackIndex = selectedTracks[type]; + if (trackIndex == DISABLED_TRACK) { + player.setRendererEnabled(type, false); + } else if (multiTrackSources[type] == null) { + player.setRendererEnabled(type, allowRendererEnable); + } else { + boolean playWhenReady = player.getPlayWhenReady(); + player.setPlayWhenReady(false); + player.setRendererEnabled(type, false); + player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK, + trackIndex); + player.setRendererEnabled(type, allowRendererEnable); + player.setPlayWhenReady(playWhenReady); + } + } + + private class InternalRendererBuilderCallback implements RendererBuilderCallback { + + private boolean canceled; + + public void cancel() { + canceled = true; + } + + @Override + public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, + TrackRenderer[] renderers) { + if (!canceled) { + DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers); + } + } + + @Override + public void onRenderersError(Exception e) { + if (!canceled) { + DemoPlayer.this.onRenderersError(e); + } + } + + } + + @Override + public void onSwitchToSteadyState(long elapsedMs) { + if (infoListener != null) { + infoListener.onSwitchToSteadyState(elapsedMs); + } + + } + + @Override + public void onAllChunksDownloaded(long totalBytes) { + if (infoListener != null) { + infoListener.onAllChunksDownloaded(totalBytes); + } + + } + +} diff --git a/src/com/mobilyzer/util/video/util/DemoUtil.java b/src/com/mobilyzer/util/video/util/DemoUtil.java new file mode 100644 index 0000000..ba91ade --- /dev/null +++ b/src/com/mobilyzer/util/video/util/DemoUtil.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.util.video.util; + +import com.google.android.exoplayer.ExoPlayerLibraryInfo; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.UUID; + +/** + * Utility methods for the demo application. + */ +public class DemoUtil { + + public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); + + public static final String CONTENT_URL_EXTRA = "content_url"; + public static final String CONTENT_TYPE_EXTRA = "content_type"; + public static final String CONTENT_ID_EXTRA = "content_id"; + + public static final int TYPE_DASH_VOD = 0; + public static final int TYPE_BBA = 1; + public static final int TYPE_PROGRESSIVE = 2; + + public static final boolean EXPOSE_EXPERIMENTAL_FEATURES = false; + + public static String getUserAgent(Context context) { + String versionName; + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + versionName = info.versionName; + } catch (NameNotFoundException e) { + versionName = "?"; + } + return "ExoPlayerDemo/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE + + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; + } + + public static byte[] executePost(String url, byte[] data, Map requestProperties) + throws MalformedURLException, IOException { + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) new URL(url).openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(data != null); + urlConnection.setDoInput(true); + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + } + if (data != null) { + OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream()); + out.write(data); + out.close(); + } + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + return convertInputStreamToByteArray(in); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + private static byte[] convertInputStreamToByteArray(InputStream inputStream) throws IOException { + byte[] bytes = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte data[] = new byte[1024]; + int count; + while ((count = inputStream.read(data)) != -1) { + bos.write(data, 0, count); + } + bos.flush(); + bos.close(); + inputStream.close(); + bytes = bos.toByteArray(); + return bytes; + } + +}