/* * 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<String, String> values; private ArrayList<HashMap<String, String>> 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<String, String>(); this.contextResults = new ArrayList<HashMap<String, String>>(); } public MeasurementDesc getMeasurementDesc(){ return this.parameters; } public DeviceProperty getDeviceProperty(){ return this.properties; } @SuppressWarnings("unchecked") public void addContextResults(ArrayList<HashMap<String, String>> contextResults) { this.contextResults = (ArrayList<HashMap<String, String>>) 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<MeasurementResult> results = new ArrayList<MeasurementResult>(); 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<MeasurementResult> results = new ArrayList<MeasurementResult>(); 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<String,String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String>(); for (int i = 0; i < valuesSize; i++) { values.put(in.readString(), in.readString()); } // contextResults = in.readArrayList(loader); contextResults= new ArrayList<HashMap<String,String>>(); int contextResultsSize=in.readInt(); for (int i = 0; i < contextResultsSize; i++) { int contextResultsHashMapSize=in.readInt(); HashMap<String,String> tempHashMap= new HashMap<String, String>(); for (int j = 0; j < contextResultsHashMapSize; j++) { tempHashMap.put(in.readString(), in.readString()); } contextResults.add(tempHashMap); } } public static final Parcelable.Creator<MeasurementResult> CREATOR = new Parcelable.Creator<MeasurementResult>() { 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<String, String> map: contextResults) { out.writeInt(map.size()); for(String s: map.keySet()){ out.writeString(s); out.writeString(map.get(s)); } } } }