apps: Add Android wifi NetworkDetailed and wpa_supplicant state graphs to viz

Display WifiStateMachine Network Detailed and wpa_supplicant states tracked
during log processing.  Other states (including the states for
WifiStateMachine itself) and/or visualizations will be added to the
visualization in a later CL.  The current graphs aim to reflect network
connectivity status.

Add tests to verify end states are added for both NetworkDetailed and
wpa_supplicant state tracking arrays.

BUG=chromium:575818
TEST=manually with Chrome OS, Brillo and Android logs
TEST=ran 'grunt test'

Change-Id: I1f5db01eddc260809372ff958779cc1b46094727
diff --git a/Gruntfile.js b/Gruntfile.js
index cc0d119..56212d4 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -21,13 +21,15 @@
     jasmine: {
       all: {
         src: [
+          './android_state_graph.js',
           './androidlog_summary.js',
-          './log_summary.js',
-          './process_log.js',
           './log_helper.js',
+          './log_summary.js',
           './manager.js',
           './netlog_summary.js',
+          './process_log.js',
           './service.js',
+          './state_graph.js',
           './syslog_summary.js',
           './wifi_state_machine.js'
         ],
diff --git a/android_state_graph.js b/android_state_graph.js
new file mode 100644
index 0000000..0def99c
--- /dev/null
+++ b/android_state_graph.js
@@ -0,0 +1,515 @@
+// Copyright 2016 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+'use strict';
+
+/**
+ * @fileoverview AndroidStateGraph class to generate d3 visualization of the
+ * wifi state machine network detailed and supplicant states.
+ * TODO: incorporate WifiStateMachine states.
+ * TODO: incorporate wifi network scan events.
+ */
+
+var androidStateGraph = {};
+
+/**
+ * This method is responsible for creating the d3 elements needed for the
+ * WifiStateMachine and supplicant state graphs.  The visualization is built
+ * with the d3 library.
+ *
+ * @param {Node} node element that will the parent of the visualization in the
+ * resulting document.
+ * @param {WifiStateMachine} wsm instance containing the log state information.
+ */
+androidStateGraph.createGraph = function(node, wsm) {
+  if (wsm.networkDetailedStates.length == 0 &&
+          wsm.supplicantStates.length == 0) {
+    // there is nothing to graph, return.
+    return;
+  }
+
+  var lineData = [wsm.networkDetailedStates];
+
+  var color = d3.scale.category10();
+
+  /**
+   * WiFiState Machine State Strings
+   *
+   * WifiStateMachine state ordering is determined by a combination of
+   * steps needed to create a connection and also to highlight a disconnect.
+   * The tracking visualization moves up the y-axis as the connection
+   * progresses.  When a disconnect or failure occurs, the line drops to the
+   * bottom of the y-axis.
+   *
+   * @const
+   */
+  var WSM_NETWORK_DETAILED_STATES = [
+    'Failed',
+    'Disconnected',
+    'Disconnecting',
+    'Suspended',
+    'Idle',
+    'Scanning',
+    'Connecting',
+    'Authenticating',
+    'ObtainingIP',
+    'Connected',
+    'CptPrtCheck',
+    'Blocked',
+    'VerifyPoorLink'
+  ];
+
+  /**
+   * Mapping for WifiStateMachine State Strings to y-axis graph value.
+   *
+   * @const
+   */
+  var WSM_STATE_STRING_TO_GRAPH_VALUE = {
+      'FAILED': 0,
+      'DISCONNECTED': 1,
+      'DISCONNECTING': 2,
+      'SUSPENDED': 3,
+      'IDLE': 4,
+      'SCANNING': 5,
+      'CONNECTING': 6,
+      'AUTHENTICATING': 7,
+      'OBTAINING_IPADDR': 8,
+      'CONNECTED': 9,
+      'CAPTIVE_PORTAL_CHECK': 10,
+      'BLOCKED': 11,
+      'VERIFYING_POOR_LINK': 12
+  };
+
+  /**
+   * Supplicant State Strings
+   *
+   * @const
+   */
+  var SUPPLICANT_STATES = [
+      'Disconnected',
+      'IntfDisabled',
+      'Inactive',
+      'Scanning',
+      'Authenticating',
+      'Associating',
+      'Associated',
+      '4WayHandsh',
+      'GroupHandsh',
+      'Completed'
+  ];
+
+  /**
+   * Mapping for Supplicant State Strings to y-axis graph value.
+   *
+   * @const
+   */
+  var SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE = {
+      'DISCONNECTED': 0,
+      'INTERFACE_DISABLED': 1,
+      'INACTIVE': 2,
+      'SCANNING': 3,
+      'AUTHENTICATING': 4,
+      'ASSOCIATING': 5,
+      'ASSOCIATED': 6,
+      '4WAY_HANDSHAKE': 7,
+      'GROUP_HANDSHAKE': 8,
+      'COMPLETED': 9
+  };
+
+  /**
+   * Visualization dom element Label.
+   *
+   * @const
+   */
+  var LABEL = 'wsm_viz';
+
+  /*
+   * Dimensions for the entire graphing area
+   *
+   * @const
+   */
+  var WIDTH_PX = 1000;
+  // @const
+  var RIGHT_MARGIN_PX = 20;
+  // @const
+  var LEFT_MARGIN_PX = 80;
+  // @const
+  var HEIGHT_PX = 800;
+  // @const
+  var TOP_MARGIN_PX = 40;
+  // @const
+  var BOTTOM_MARGIN_PX = 40;
+
+  /*
+   * Stroke width and spacing for plots, text and labels.
+   *
+   * @const
+   */
+  var STROKE_WIDTH_PX = 2;
+  // @const
+  var VERTICAL_SPACE_PX = 6;
+  // @const
+  var HORIZONTAL_SPACE_PX = 4;
+
+  /*
+   *  General structure for the visualization layout.
+   *
+   * +-------------------------------------------------------+ -
+   * |                                                       |  TOP_MARGIN_PX
+   * |   +------------------------------------------------+  | -
+   * |   |   Zoomed State Graph                           |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   +------------------------------------------------+  |
+   * |                                                       |
+   * |   +------------------------------------------------+  |
+   * |   |   Full State Graph                             |  |
+   * |   +------------------------------------------------+  |
+   * |                                                       |
+   * |   +------------------------------------------------+  |
+   * |   |   Supplicant Graph                             |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   |                                                |  |
+   * |   +------------------------------------------------+  | -
+   * |                                                       |  BOTTOM_MARGIN_PX
+   * +-------------------------------------------------------+ -
+   * |   |                                                |  |
+   *   LEFT_MARGIN_PX                                       RIGHT_MARGIN_PX
+   *
+  */
+  var svgDomNode = d3.select(node).append('svg:svg')
+    .attr('id', LABEL)
+    .attr('width', WIDTH_PX)
+    .attr('height', HEIGHT_PX);
+
+  /*
+   * Define heights of subgraphs
+   *
+   * @const
+   */
+  var ZOOMED_STATE_GRAPH_HEIGHT_PX = 450;
+  // @const
+  var FULL_STATE_GRAPH_HEIGHT_PX = 40;
+  // @const
+  var SUPPLICANT_GRAPH_HEIGHT_PX = 180;
+
+  /*
+   * Define tick size for graphs.
+   *
+   * @const
+   */
+  var AXIS_TICK_SIZE_PX = 5;
+
+  // Define margins for individual graphs
+  var zoomedStateGraphMargin = {
+    top: TOP_MARGIN_PX,
+    right: RIGHT_MARGIN_PX,
+    bottom: BOTTOM_MARGIN_PX,
+    left: LEFT_MARGIN_PX
+  };
+
+  var fullStateGraphMargin = {
+    top: ZOOMED_STATE_GRAPH_HEIGHT_PX,
+    right: RIGHT_MARGIN_PX,
+    bottom: BOTTOM_MARGIN_PX,
+    left: LEFT_MARGIN_PX
+  };
+
+  var supplicantGraphMargin = {
+    top: HEIGHT_PX - BOTTOM_MARGIN_PX - SUPPLICANT_GRAPH_HEIGHT_PX,
+    right: RIGHT_MARGIN_PX,
+    bottom: BOTTOM_MARGIN_PX,
+    left: LEFT_MARGIN_PX
+  };
+
+  // Define x- and y-axis values and lines for WifiStateMachine zoomedStateGraph
+  var zoomedStateGraphXAxis = d3.scale.linear()
+      .domain([0, wsm.endTime - wsm.startTime])
+      .range([0, WIDTH_PX - zoomedStateGraphMargin.right -
+          zoomedStateGraphMargin.left]);
+  var zoomedStateGraphYAxis = d3.scale.linear()
+      .domain([0, Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length - 1])
+      .range([ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
+          zoomedStateGraphMargin.top, zoomedStateGraphMargin.top]);
+
+  // Define x- and y-axis values and lines for WifiStateMachine fullStateGraph
+  var fullStateGraphXAxis = d3.scale.linear()
+      .domain(zoomedStateGraphXAxis.domain())
+      .range([0,
+              WIDTH_PX - fullStateGraphMargin.right -
+                  fullStateGraphMargin.left]);
+  var fullStateGraphYAxis = d3.scale.linear()
+      .domain([0, Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length - 1])
+      .range([FULL_STATE_GRAPH_HEIGHT_PX, 0]);
+
+  // define x- and y-axis values and lines for supplicantStateGraph
+  var supplicantGraphXAxis = d3.scale.linear()
+      .domain(zoomedStateGraphXAxis.domain())
+      .range([0,
+              WIDTH_PX - supplicantGraphMargin.right -
+                  supplicantGraphMargin.left]);
+  var supplicantGraphYAxis = d3.scale.linear()
+      .domain([0,
+               Object.keys(SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE).length - 1])
+      .range([SUPPLICANT_GRAPH_HEIGHT_PX, 0]);
+
+  /*
+   * Axis are generated using d3.svg.axis functions.  These functions create the
+   * svg elemts to draw the axis, ticks and labels on the screen.  The axis
+   * details are defined in the following objects.  The axis are not appended to
+   * the graph until a selection is made.  At that point, the selection will
+   * call the appropriate axis function to generate and append the axis svg
+   * elements to the DOM.  This function is described below.
+   *
+   * Example:
+   *   zoomedStateGraph.append('g')
+   *       .attr('class', 'x axis')
+   *       .attr('transform', 'translate(0,' +
+   *           (ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
+   *              zoomedStateGraphMargin.top) + ')')
+   *       .call(createStateGraphZoomedXAxis);
+   *
+   * zoomedStateGraph is the svg element in the DOM - where the axis elements
+   *     will be added.
+   * We append a group element ('g') to the zoomedStateGraph.  This group
+   *     element allows us to contain/group other elements and apply
+   *     transformations (used to position groups of svg elements by adapting
+   *     x- and y-coordinates).  This group does not result in a visible element
+   *     in the page.
+   * Attributes for the new group element are added (class name and transform).
+   * The function call() is then called on the new group element.  The call()
+   *     function takes the incoming selection (new group element) and hands it
+   *     to a function (createStateGraphZoomedXAxis) to create and append the
+   *     axis svg elements (ticks and labels).
+   *
+   * Note: The specification for the axis could be directly entered in the call
+   * function without creating the createStateGraphZoomedXAxis, but this would
+   * introduce duplicate x-axis specifications for the default case
+   * (zoomedStateGraph displays the same data as fullStateGraph) and when a
+   * portion of the fullStateGraph is selected and onBrushed() is called to
+   * display a subset of the data in the zoomedStateGraph.
+   */
+
+  var createStateGraphZoomedXAxis = d3.svg.axis()
+      .scale(zoomedStateGraphXAxis)
+      .tickSize(AXIS_TICK_SIZE_PX)
+      .tickSubdivide(true)
+      .tickFormat(getXTickLabels);
+
+  var createStateGraphZoomedYAxis = d3.svg.axis()
+      .scale(zoomedStateGraphYAxis)
+      .tickSize(AXIS_TICK_SIZE_PX)
+      .innerTickSize(-WIDTH_PX + zoomedStateGraphMargin.left +
+          zoomedStateGraphMargin.right)
+      .ticks(Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length)
+      .orient('left')
+      .tickSubdivide(true)
+      .tickFormat(function(stateIndex) {
+        return WSM_NETWORK_DETAILED_STATES[stateIndex];
+      });
+
+  var createFullStateGraphXAxis = d3.svg.axis()
+      .scale(fullStateGraphXAxis)
+      .orient('bottom')
+      .tickFormat(getXTickLabels);
+
+  var createSupplicantGraphYAxis = d3.svg.axis()
+      .scale(supplicantGraphYAxis)
+      .tickSize(AXIS_TICK_SIZE_PX)
+      .innerTickSize(-WIDTH_PX + zoomedStateGraphMargin.left +
+          zoomedStateGraphMargin.right)
+      .ticks(Object.keys(SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE).length)
+      .orient('left')
+      .tickSubdivide(true)
+      .tickFormat(function(stateIndex) {
+        return SUPPLICANT_STATES[stateIndex];
+      });
+
+  var createSupplicantXAxis = d3.svg.axis()
+      .scale(supplicantGraphXAxis)
+      .tickSize(AXIS_TICK_SIZE_PX)
+      .tickSubdivide(true)
+      .tickFormat(getXTickLabels);
+
+  var brush = d3.svg.brush()
+      .x(fullStateGraphXAxis)
+      .on('brush', onBrushed);
+
+  var createZoomedStateGraphDataPoint = d3.svg.line()
+      .interpolate('linear')
+      .x(function(wsmState) {
+        return zoomedStateGraphXAxis(wsmState.x);
+      })
+      .y(function(wsmState) {
+        return zoomedStateGraphYAxis(
+            WSM_STATE_STRING_TO_GRAPH_VALUE[wsmState.y]);
+      });
+
+  var createFullStateGraphDataPoint = d3.svg.line()
+      .interpolate('linear')
+      .x(function(wsmState) {
+        return fullStateGraphXAxis(wsmState.x);
+      })
+      .y(function(wsmState) {
+        return fullStateGraphYAxis(WSM_STATE_STRING_TO_GRAPH_VALUE[wsmState.y]);
+      });
+
+  var createSupplicantZoomedDataPoint = d3.svg.line()
+      .interpolate('linear')
+      .x(function(supplicantState) {
+        return supplicantGraphXAxis(supplicantState.x);
+      })
+      .y(function(supplicantState) {
+        return supplicantGraphYAxis(
+            SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE[supplicantState.y]);
+      });
+
+  svgDomNode.selectAll('legend')
+      .data(lineData)
+      .enter()
+      .append('text')
+      .attr('class', 'legend')
+      .attr('x', function(wsm, wsmIndex) {
+        return wsmIndex * (HORIZONTAL_SPACE_PX * zoomedStateGraphMargin.right);
+      })
+      .attr('y', (ZOOMED_STATE_GRAPH_HEIGHT_PX - VERTICAL_SPACE_PX))
+      .attr('fill', function(wsm, wsmIndex) {
+        return wsm.color = color(wsmIndex);
+      })
+      .text(function(wsm, wsmIndex) {
+        return 'WifiNetworkDetailed ' + wsmIndex + ' states';
+      });
+
+  svgDomNode.append('svg:g')
+      .attr('class', 'y axis')
+      .attr('transform', 'translate(' + zoomedStateGraphMargin.left + ',0)')
+      .call(createStateGraphZoomedYAxis);
+
+  svgDomNode.append('defs').append('clipPath')
+      .attr('id', 'clip')
+      .append('rect')
+      .attr('width',
+            WIDTH_PX - zoomedStateGraphMargin.left -
+                zoomedStateGraphMargin.right)
+      .attr('height',
+            ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.top -
+                zoomedStateGraphMargin.bottom);
+
+  var zoomedStateGraph = svgDomNode.append('g')
+      .attr('class', 'zoomedStateGraph')
+      .attr('transform', 'translate(' + zoomedStateGraphMargin.left + ',0)');
+
+  var fullStateGraph = svgDomNode.append('g')
+      .attr('class', 'fullStateGraph')
+      .attr('transform',
+            'translate(' + fullStateGraphMargin.left + ',' +
+              fullStateGraphMargin.top + ')');
+
+  var supplicant = svgDomNode.append('g')
+      .attr('transform',
+            'translate(' + supplicantGraphMargin.left + ',' +
+                supplicantGraphMargin.top + ')');
+
+  /*
+   * Append graph data points, axis and brush elements to the graphs.
+   */
+  zoomedStateGraph.append('g')
+      .attr('class', 'x axis')
+      .attr('transform', 'translate(0,' +
+          (ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
+             zoomedStateGraphMargin.top) + ')')
+      .call(createStateGraphZoomedXAxis);
+
+  zoomedStateGraph.selectAll('data')
+      .data(lineData)
+      .enter()
+      .append('path')
+      .attr('stroke', function(wsmLine, wsmId) {
+        return wsmLine.color = color(wsmId);
+      })
+      .attr('stroke-width', STROKE_WIDTH_PX)
+      .attr('fill', 'none')
+      .attr('class', function(wsmLine, wsmId) {
+        return 'area_' + wsmId + ' data';
+      })
+      .attr('clip-path', 'url(#clip)')
+      .attr('d', createZoomedStateGraphDataPoint);
+
+
+
+  fullStateGraph.selectAll('path')
+      .data(lineData)
+      .enter()
+      .append('path')
+      .attr('class', function(wsmLine, wsmId) {
+        return 'line_' + wsmId;
+      })
+      .attr('d', createFullStateGraphDataPoint)
+      .attr('stroke', function(wsmLine, wsmId) {
+        return wsmLine.color = color(wsmId);
+      })
+      .attr('stroke-width', STROKE_WIDTH_PX)
+      .attr('fill', 'none');
+
+  fullStateGraph.append('g')
+      .attr('class', 'x axis')
+      .attr('transform', 'translate(0,' + FULL_STATE_GRAPH_HEIGHT_PX + ')')
+      .call(createFullStateGraphXAxis);
+
+  fullStateGraph.append('g')
+      .attr('class', 'x brush')
+      .call(brush)
+      .selectAll('rect')
+      .attr('y', -VERTICAL_SPACE_PX)
+      .attr('height',
+            FULL_STATE_GRAPH_HEIGHT_PX + VERTICAL_SPACE_PX);
+
+  supplicant.append('g')
+      .attr('class', 'x axis')
+      .attr('transform',
+            'translate(0,' + SUPPLICANT_GRAPH_HEIGHT_PX + ')')
+      .call(createSupplicantXAxis);
+
+  supplicant.append('g')
+      .attr('class', 'y axis')
+      .attr('transform', 'translate(0,0)')
+      .call(createSupplicantGraphYAxis);
+
+  supplicant.append('text')
+      .attr('x', -supplicantGraphMargin.left)
+      .attr('y',
+            SUPPLICANT_GRAPH_HEIGHT_PX + supplicantGraphMargin.bottom -
+                VERTICAL_SPACE_PX)
+      .attr('class', 'supplicant')
+      .text('wpa_supplicant state');
+
+  supplicant.append('path')
+      .attr('fill', 'none')
+      .attr('class', 'supp_line')
+      .attr('clip-path', 'url(#clip)')
+      .attr('d', createSupplicantZoomedDataPoint(wsm.supplicantStates));
+
+
+  function onBrushed() {
+    zoomedStateGraphXAxis.domain(
+        brush.empty() ? fullStateGraphXAxis.domain() : brush.extent());
+    supplicantGraphXAxis.domain(
+        brush.empty() ? fullStateGraphXAxis.domain() : brush.extent());
+    zoomedStateGraph.selectAll('path.data')
+        .attr('d', createZoomedStateGraphDataPoint);
+    supplicant.select('path.supp_line')
+        .attr('d', createSupplicantZoomedDataPoint(wsm.supplicantStates));
+    zoomedStateGraph.select('.x.axis').call(createStateGraphZoomedXAxis);
+    supplicant.select('.x.axis').call(createSupplicantXAxis);
+  }
+
+  function getXTickLabels(elapsedMS) {
+    var format = d3.time.format('%H:%M:%S');
+    var date = new Date(wsm.startTime + elapsedMS);
+    return format(date);
+  }
+}
diff --git a/androidlog_summary.js b/androidlog_summary.js
index 59b1817..481dd1f 100644
--- a/androidlog_summary.js
+++ b/androidlog_summary.js
@@ -46,7 +46,7 @@
       var oldState = result[1];
       var newState = result[2];
       var stateTimestamp = processingState.time;
-      var states = processingState.wifiStateMachine.states;
+      var states = processingState.wifiStateMachine.networkDetailedStates;
       var newStateStart = {'x': stateTimestamp, 'y': newState};
       var prevStateEnd = {'x': stateTimestamp, 'y': oldState};
       if (states.length > 0) {
@@ -150,7 +150,7 @@
     }
 
     if (parsedLogLineTime < logSummary.logEndTime) {
-      console.log('warning: log time has rolled back, check timezone');
+      console.log('warning: log time has rolled back.');
     } else {
       logSummary.logEndTime = parsedLogLineTime;
     }
@@ -177,6 +177,7 @@
 
     logText.push(tempLogText);
   }
+
   if (logSummary.wifiStateMachines.length > 0) {
     logSummary.wifiStateMachines[logSummary.wifiStateMachines.length - 1]
         .endTime = logSummary.logEndTime;
diff --git a/process_log.js b/process_log.js
index 6f0e40a..3e11747 100644
--- a/process_log.js
+++ b/process_log.js
@@ -117,7 +117,6 @@
  * @param {LogSummary} logSummary Object to hold state read from log.
  * @private
  */
-
 function displayAndroidLogSummary(logSummary) {
   console.log('inside android log summary!');
   var wsm = document.getElementsByClassName('managers')[0];
@@ -128,19 +127,46 @@
   var timeInfo = document.createElement('p');
   timeInfo.innerText = 'Total Log Time: ' +
       logHelper.formatElapsedMS(logSummary.logEndTime -
-          logSummary.logStartTime);
+                                logSummary.logStartTime);
   wsm.appendChild(timeInfo);
 
   var wifiStateMachines = logSummary.wifiStateMachines;
   var wsmInfo;
   for (var i = 0; i < wifiStateMachines.length; i++) {
+    var stateMachine = logSummary.wifiStateMachines[i];
     wsmInfo = document.createElement('p');
-    wsmInfo.innerText = 'WifiStateMachine ' + wifiStateMachines[i].id + ": " +
-         logHelper.formatElapsedMS(wifiStateMachines[i].endTime -
-                                   wifiStateMachines[i].startTime);
-     wsm.appendChild(wsmInfo);
+    wsmInfo.innerText = 'WifiStateMachine ' + stateMachine.id + ': ' +
+         logHelper.formatElapsedMS(stateMachine.endTime -
+                                   stateMachine.startTime);
+    wsm.appendChild(wsmInfo);
 
-    //TODO: add post processing for WSM states before graphing
+    // post processing for WSM states before graphing
+    var xTime;
+    var graphData = stateMachine.networkDetailedStates;
+    for (var j = 0; j < graphData.length; j++) {
+      if (graphData[j].x != 0) {
+        xTime = Date.parse(graphData[j].x) - stateMachine.startTime;
+      } else {
+        xTime = 0;
+      }
+      graphData[j].x = xTime;
+    }
+    if (graphData.length > 0) {
+      graphData.push({'x': stateMachine.endTime - stateMachine.startTime,
+                      'y': graphData[graphData.length - 1].y});
+    }
+    console.log('wifiSM check: ', stateMachine);
+
+    // post processing for supplicant states before graphing
+    var suppStates = stateMachine.supplicantStates;
+    if (suppStates.length > 0) {
+      var lastState = suppStates[suppStates.length - 1];
+      suppStates.push({'x': stateMachine.endTime - stateMachine.startTime,
+                       'y': lastState.y});
+    }
+    console.log('supplicantData check: ', stateMachine.supplicantStates);
+
+    androidStateGraph.createGraph(wsm, stateMachine);
   }
 
   //Now add in notes for the WSM
diff --git a/service_states.html b/service_states.html
index e1cc89d..a31bd51 100644
--- a/service_states.html
+++ b/service_states.html
@@ -15,11 +15,13 @@
     <script src="syslog_summary.js"></script>
     <script src="androidlog_summary.js"></script>
     <script src="wifi_state_machine.js"></script>
+    <script src="android_state_graph.js"></script>
   </head>
   <body>
-    <h1>ChromeOS Network Log Processor</h1>
-    <p>Please select a log for processing.  Supported files are ChromeOS
-         system logs and ChromeOS net.log files. </p>
+    <h1>Chrome OS, Brillo and Android Network Log Processor</h1>
+    <p>Please select a log for processing.  Supported files are Chrome OS
+         system logs and Chrome OS net.log files, Brillo net.log files and
+         Android bugreport and logcat files. </p>
     <input type="file" id="file_name">
     <!--<input type="file" id="file_name" onchange="parseFile();">-->
     <div class="managers">
diff --git a/test/spec/AndroidlogSummarySpec.js b/test/spec/AndroidlogSummarySpec.js
index 00d93bf..e6def37 100644
--- a/test/spec/AndroidlogSummarySpec.js
+++ b/test/spec/AndroidlogSummarySpec.js
@@ -63,7 +63,8 @@
     // and stop point.  The last state will get the endpoint in post processing
     // once the end of the log is found.
     expect(logSummary.wifiStateMachines[0].supplicantStates.length).toBe(7);
-    expect(logSummary.wifiStateMachines[0].states.length).toBe(0);
+    expect(
+        logSummary.wifiStateMachines[0].networkDetailedStates.length).toBe(0);
     expect(logSummary.wifiStateMachines[0].notes.length).toBe(3);
   });
 
@@ -84,7 +85,8 @@
     // entries in the state array since all but the last state will have a start
     // and stop point.  The last state will get the endpoint in post processing
     // once the end of the log is found.
-    expect(logSummary.wifiStateMachines[0].states.length).toBe(7);
+    expect(
+        logSummary.wifiStateMachines[0].networkDetailedStates.length).toBe(7);
     expect(logSummary.wifiStateMachines[0].notes.length).toBe(3);
   });
 
@@ -108,7 +110,8 @@
     // processing.
     expect(logSummary.wifiStateMachines[0].supplicantStates.length).toBe(3);
     // WSM states should have 3 entries: 2 for old state, 1 for new state
-    expect(logSummary.wifiStateMachines[0].states.length).toBe(3);
+    expect(
+        logSummary.wifiStateMachines[0].networkDetailedStates.length).toBe(3);
     // Notes should have 3 entries:
     // 1 - supplicant disconnect
     // 2 - WSM state change
diff --git a/test/spec/ProcessLogSpec.js b/test/spec/ProcessLogSpec.js
index 875f3fe..5c7db92 100644
--- a/test/spec/ProcessLogSpec.js
+++ b/test/spec/ProcessLogSpec.js
@@ -44,4 +44,163 @@
     parseFile();
     expect(readFileSpy.calls.any()).toEqual(false);
   });
+
+  it('should add end state for supplicant states for Android logs',
+      function() {
+    var logSummary = new LogSummary();
+    var wsm = new WifiStateMachine(0, 0, 0);
+    logSummary.wifiStateMachines = [wsm];
+    wsm.endTime = 10;
+
+    wsm.supplicantStates.push({'x': 0, 'y': 'SCANNING'});
+    wsm.supplicantStates.push({'x': 1, 'y': 'SCANNING'});
+    wsm.supplicantStates.push({'x': 1, 'y': 'ASSOCIATING'});
+
+    spyOn(androidStateGraph, 'createGraph');  // Mock out call
+
+    displayAndroidLogSummary(logSummary);
+
+    expect(wsm.supplicantStates.length).toBe(4);
+    expect(wsm.supplicantStates[3].x).toBe(10);
+    expect(wsm.supplicantStates[3].y).toBe('ASSOCIATING');
+  });
+
+  it('should add end state for single supplicant state for Android logs',
+      function() {
+    var logSummary = new LogSummary();
+    var wsm = new WifiStateMachine(0, 0, 0);
+    logSummary.wifiStateMachines = [wsm];
+    wsm.endTime = 10;
+
+    wsm.supplicantStates.push({'x': 0, 'y': 'SCANNING'});
+
+    spyOn(androidStateGraph, 'createGraph');
+
+    displayAndroidLogSummary(logSummary);
+
+    expect(wsm.supplicantStates.length).toBe(2);
+    expect(wsm.supplicantStates[1].x).toBe(10);
+    expect(wsm.supplicantStates[1].y).toBe('SCANNING');
+  });
+
+  it('should add end state for WifiStateMachine states for Android logs',
+      function() {
+    var logSummary = new LogSummary();
+    var wsm = new WifiStateMachine(0, 0, 0);
+    logSummary.wifiStateMachines = [wsm];
+    wsm.endTime = 10;
+
+    wsm.networkDetailedStates.push({'x': 0, 'y': 'SCANNING'});
+    wsm.networkDetailedStates.push({'x': 1, 'y': 'SCANNING'});
+    wsm.networkDetailedStates.push({'x': 1, 'y': 'ASSOCIATING'});
+
+    spyOn(androidStateGraph, 'createGraph');
+
+    displayAndroidLogSummary(logSummary);
+
+    expect(wsm.networkDetailedStates.length).toBe(4);
+    expect(wsm.networkDetailedStates[3].x).toBe(10);
+    expect(wsm.networkDetailedStates[3].y).toBe('ASSOCIATING');
+  });
+
+  it('should add end state for single WifiStateMachine state for Android logs',
+      function() {
+    var logSummary = new LogSummary();
+    var wsm = new WifiStateMachine(0, 0, 0);
+    logSummary.wifiStateMachines = [wsm];
+    wsm.endTime = 10;
+
+    wsm.networkDetailedStates.push({'x': 0, 'y': 'SCANNING'});
+
+    spyOn(androidStateGraph, 'createGraph');
+
+    displayAndroidLogSummary(logSummary);
+
+    expect(wsm.networkDetailedStates.length).toBe(2);
+    expect(wsm.networkDetailedStates[1].x).toBe(10);
+    expect(wsm.networkDetailedStates[1].y).toBe('SCANNING');
+  });
+
+  it('should add end state for supplicant states for shill logs',
+      function() {
+    var logSummary = new LogSummary();
+    var manager = new Manager(0, 0, 0);
+    logSummary.managers = [manager];
+    manager.endTime = 10;
+
+    manager.supplicantStates.push({'x': 0, 'y': 'SCANNING'});
+    manager.supplicantStates.push({'x': 1, 'y': 'SCANNING'});
+    manager.supplicantStates.push({'x': 1, 'y': 'ASSOCIATING'});
+
+    spyOn(stateGraph, 'createGraph');
+
+    displayLogSummary(logSummary);
+
+    expect(manager.supplicantStates.length).toBe(4);
+    expect(manager.supplicantStates[3].x).toBe(10);
+    expect(manager.supplicantStates[3].y).toBe('ASSOCIATING');
+  });
+
+  it('should add end state for single supplicant state for shill logs',
+      function() {
+    var logSummary = new LogSummary();
+    var manager = new Manager(0, 0, 0);
+    logSummary.managers = [manager];
+    manager.endTime = 10;
+
+    manager.supplicantStates.push({'x': 0, 'y': 'SCANNING'});
+
+    spyOn(stateGraph, 'createGraph');
+
+    displayLogSummary(logSummary);
+
+    expect(manager.supplicantStates.length).toBe(2);
+    expect(manager.supplicantStates[1].x).toBe(10);
+    expect(manager.supplicantStates[1].y).toBe('SCANNING');
+  });
+
+  it('should add end state for service states for shill logs',
+      function() {
+    var logSummary = new LogSummary();
+    var manager = new Manager(0, 0, 0);
+    logSummary.managers = [manager];
+    manager.endTime = 10;
+    var service = new Service(0);
+    service.isActive = true;
+
+    service.graphData.push({'x': 0, 'y': 'SCANNING'});
+    service.graphData.push({'x': 1, 'y': 'SCANNING'});
+    service.graphData.push({'x': 1, 'y': 'ASSOCIATING'});
+    manager.services.push(service);
+
+    spyOn(stateGraph, 'createGraph');
+
+    displayLogSummary(logSummary);
+
+    expect(service.graphData.length).toBe(4);
+    expect(service.graphData[3].x).toBe(10);
+    expect(service.graphData[3].y).toBe('ASSOCIATING');
+  });
+
+  it('should add end state for single service state for shill logs',
+      function() {
+    var logSummary = new LogSummary();
+    var manager = new Manager(0, 0, 0);
+    logSummary.managers = [manager];
+    manager.endTime = 10;
+    var service = new Service(0);
+    service.isActive = true;
+
+    service.graphData.push({'x': 0, 'y': 'SCANNING'});
+    manager.services.push(service);
+
+    spyOn(stateGraph, 'createGraph');
+
+    displayLogSummary(logSummary);
+
+    expect(service.graphData.length).toBe(2);
+    expect(service.graphData[1].x).toBe(10);
+    expect(service.graphData[1].y).toBe('SCANNING');
+  });
+
 });
diff --git a/wifi_state_machine.js b/wifi_state_machine.js
index 6d4a8ab..2af8e84 100644
--- a/wifi_state_machine.js
+++ b/wifi_state_machine.js
@@ -13,16 +13,14 @@
  *
  * @param {String} id Identifier for new WifiStateMachine instance.
  * @param {long} ms WifiStateMachine start time in ms.
- * @param {long} offset WifiStateMachine log time offset in ms.
  */
-function WifiStateMachine(id, ms, offset) {
+function WifiStateMachine(id, ms) {
   this.id = id;
   this.startTime = ms;
   this.scans = 0;
   this.scanDetails = [];
-  this.states = [];
+  this.networkDetailedStates = [];
   this.endTime = -1;
-  this.timeOffset = offset;
   this.supplicantStates = [];
 }