apps: add log type detection for android logcat and bugreport files

Detect main subsection of Android logcat and bugreport files.  This is
the first step to processing Android logs.  Added tests for both logcat
and bugreport detection.

BUG=chromium:568337
TEST=ran 'grunt test'
TEST=manually tested with log files for chromeos, brillo and android

Change-Id: I3bf5f4669f62c62888ff249fabe27f6de4ffe7c2
diff --git a/Gruntfile.js b/Gruntfile.js
index 7a99778..fd4d27e 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -21,6 +21,7 @@
     jasmine: {
       all: {
         src: [
+          './androidlog_summary.js',
           './log_summary.js',
           './process_log.js',
           './log_helper.js',
diff --git a/androidlog_summary.js b/androidlog_summary.js
new file mode 100644
index 0000000..d0ead0d
--- /dev/null
+++ b/androidlog_summary.js
@@ -0,0 +1,117 @@
+// Copyright 2015 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 Class to hold methods for processing syslog messages.
+ */
+
+var androidlogSummary = {};
+
+/**
+ * Object used for passing state between main processing loop and
+ * regeular expression handler functions.
+ * @constructor
+ * @type {Object}
+ * @param {LogSummary} logSummary object holding log state after processing
+ * @param {String} time timestamp of current log line
+ * @param {String[]} results results from regex test
+ */
+function AndroidlogProcessingState(logSummary, time, results) {
+  this.logSummary = logSummary;
+  this.time = time;
+  this.results = results;
+}
+
+/**
+ * RegExp for android log line.
+ * @type {RegExp}
+ */
+androidlogSummary.CONNECTIVITY_SERVICE = / ConnectivityService: /;
+
+/**
+ * Array of Objects holding regexs and handlers for Android subsections
+ * @type {Object[]}
+ * @return {string} handler functions return log line tag label
+ */
+androidlogSummary.taggedLines = {
+  'supplicant_disconnected': {
+    re: /wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED /,
+    handler: function(processingState, result) {
+      // TODO: this will be added to a summary notes section
+    }
+  }
+};
+
+/**
+ * This method processes each log line in the supplied String array.  Each log
+ * line is checked to see if matches one of the regular expressions from the
+ * logHelper.  Each flagged log line is then processed.
+ *
+ * @param {String[]} logLines String array for lines in a log.
+ * @param {LogSummary} logSummary Object to hold processed log state.
+ * @return {Object[]} Returns an Object[] of text and anchor labels if the log
+ * was processed, null if the log is not a recognized format.
+ */
+androidlogSummary.processLogLines = function(logLines, logSummary) {
+  var logStart = -1;
+  var time;
+  var logText = [];
+  var timeCheck;
+  var result = null;
+  var processingState = new ProcessingState(logSummary, null, null, null);
+  var logType = 'android';
+
+  for (var i = 0; i < logLines.length; i++) {
+    console.log('processing log line: ' + logLines[i]);
+    time = logLines[i].match(logHelper.ANDROID_TIME_FORMAT);
+    processingState.time = time;
+    if (time == null) {
+      console.log('not a valid time...  continue');
+      continue;
+    }
+    timeCheck = Date.parse(time[0]);
+
+    var tempLogText = {text: logLines[i],
+                       tag: time[0],
+                       type: 'basic',
+                       file: logType,
+                       ts: timeCheck};
+    result = null;
+    for (var taggedLine in androidlogSummary.taggedLines) {
+      var result = logLines[i].match(
+          androidlogSummary.taggedLines[taggedLine].re);
+      if (result != null) {
+        tempLogText.type =
+            androidlogSummary.taggedLines[taggedLine].handler(processingState,
+                                                              result);
+        break;
+      }
+    }
+    if (!result) {
+      tempLogText.tag = null;
+    }
+
+    logText.push(tempLogText);
+  }
+  return logText;
+};
+
+/**
+ * This method processes each log line in the supplied String array to determine
+ * if the log is an android log.
+ *
+ * @param {String[]} logLines String array for lines in a log.
+ * @return {boolean} Returns true is the log is an android log, false otherwise.
+ */
+androidlogSummary.androidlogTypeCheck = function(logLines) {
+  for (var i = 0; i < logLines.length; i++) {
+    if (logHelper.ANDROID_MAIN_SECTION.test(logLines[i]) ||
+        logHelper.ANDROID_BUG_REPORT.test(logLines[i]) ||
+        androidlogSummary.CONNECTIVITY_SERVICE.test(logLines[i])) {
+        return true;
+    }
+  }
+  return false;
+};
diff --git a/log_helper.js b/log_helper.js
index 5c92729..372cc79 100644
--- a/log_helper.js
+++ b/log_helper.js
@@ -40,6 +40,27 @@
 logHelper.BRILLO_LOG_START = /--------- beginning of main/;
 
 /**
+ * RegExp for start of main log section.
+ * @type {RegExp}
+ */
+logHelper.ANDROID_MAIN_SECTION = /--------- beginning of main/;
+
+/**
+ * RegExp for end of Android log section.
+ * @type {RegExp}
+ */
+logHelper.ANDROID_SECTION_END = /\[logcat: \d+.\d+s elapsed\]/;
+
+
+
+/**
+ * RegExp for start of Android bug report file
+ * @type {RegExp}
+ */
+logHelper.ANDROID_BUG_REPORT =
+    /== dumpstate: \d{4}-\d{2}-\d{2}.\d{2}:\d{2}:\d{2}/;
+
+/**
  * RegExp for log timestamp.
  * Sample timestamp from ChomeOS net.log:
  *   2015-10-28T15:40:30.879026-05:00
@@ -61,6 +82,14 @@
     /(\d{2}-\d{2}.\d{2}:\d{2}:\d{2}.\d{3})(?: +[1-9][0-9]* +[1-9][0-9]*)/;
 
 /**
+ * RegExp for Android log timestamp.
+ * Sample valid timestamp from Android log:
+ *   11-20 12:08:06.412
+ * @type {RegExp}
+ */
+logHelper.ANDROID_TIME_FORMAT = /(\d{2}-\d{2}.\d{2}:\d{2}:\d{2}.\d{3})/;
+
+/**
  * Method to get time offset from log line.
  *
  * @param {Object} timeMatchResults results from matching regex.
@@ -126,6 +155,42 @@
  * @return  {int} returns new index into supplied String[]
  */
 logHelper.logDetectors = {
+  'android_bugreport': {
+    re: logHelper.ANDROID_BUG_REPORT,
+    handler: function(logHolder, logLines, i) {
+      logHolder.fileType = 'android_log';
+      while (i < logLines.length) {
+        if (logHelper.ANDROID_MAIN_SECTION.test(logLines[i])) {
+          // found start of main log
+          console.log('found start of main section');
+          break;
+        }
+        i++;
+      }
+      logHolder.androidlog = logHelper.getSubLog(logLines, i);
+      console.log('found an android bug report: ' +
+          logHolder.androidlog.length);
+      return i + logHolder.androidlog.length;
+    }
+  },
+  'android': {
+    re: logHelper.ANDROID_MAIN_SECTION,
+    handler: function(logHolder, logLines, i) {
+      var tempLogHolder = logHelper.getSubLog(logLines, i);
+      if (androidlogSummary.androidlogTypeCheck(tempLogHolder)) {
+        logHolder.androidlog = tempLogHolder;
+        logHolder.fileType = 'android_log';
+      } else if (netlogSummary.brillologTypeCheck(tempLogHolder)) {
+        logHolder.brillolog = tempLogHolder;
+        logHolder.fileType = 'brillo_log';
+      } else {
+        console.log('Error in log type detection - ' +
+                    'android time stamp but not android or brillo log');
+        return i;
+      }
+      return i + tempLogHolder.length;
+    }
+  },
   'netlog_multiline': {
     re: logHelper.NET_LOG_START,
     handler: function(logHolder, logLines, i) {
@@ -145,15 +210,6 @@
       console.log('found the syslog section!: ' + logHolder.syslog.length);
       return i + logHolder.syslog.length;
     }
-  },
-  'brillo': {
-    re: logHelper.BRILLO_LOG_START,
-    handler: function(logHolder, logLines, i) {
-      logHolder.fileType = 'brillo_log';
-      logHolder.brillolog = logHelper.getSubLog(logLines, i);
-      console.log('found a brillo log!: ' + logHolder.brillolog.length);
-      return i + logHolder.brillolog.length;
-    }
   }
 };
 
@@ -181,7 +237,11 @@
   }
 
   if (logHolder.fileType == 'unknown') {
-    if (netlogSummary.netlogTypeCheck(logLines)) {
+    if (androidlogSummary.androidlogTypeCheck(logLines)) {
+      // supplied file is an android log file
+      logHolder.fileType = 'androidlog';
+      logHolder.netlog = logLines;
+    } else if (netlogSummary.netlogTypeCheck(logLines)) {
       // supplied file is netlog
       logHolder.fileType = 'netlog';
       logHolder.netlog = logLines;
@@ -215,9 +275,15 @@
       // subsection is done...
       return subLog;
     }
+    if (logHelper.ANDROID_SECTION_END.test(logLines[i])) {
+      // subsection is done...
+      return subLog;
+    }
     var time = logLines[i].match(logHelper.TIME_FORMAT);
     if (time == null) {
-      console.log('try Brillo time format...');
+      time = logLines[i].match(logHelper.ANDROID_TIME_FORMAT);
+    }
+    if (time == null) {
       time = logLines[i].match(logHelper.BRILLO_TIME_FORMAT);
     }
     if (time == null) {
diff --git a/process_log.js b/process_log.js
index 5daf2f4..d2850c8 100644
--- a/process_log.js
+++ b/process_log.js
@@ -55,6 +55,11 @@
         return;
       }
 
+      if (logHolder.androidlog) {
+        logHolder.androidlog = androidlogSummary.processLogLines(
+            logHolder.androidlog, logSummary);
+      }
+
       if (logHolder.brillolog) {
         logHolder.brillolog = netlogSummary.processLogLines(logHolder.brillolog,
                                                             logSummary);
@@ -65,7 +70,6 @@
         logHolder.netlog = netlogSummary.processLogLines(logHolder.netlog,
                                                          logSummary);
       }
-      console.log('manager[0].notes: ', logSummary.managers[0].notes);
       if (logHolder.syslog) {
         console.log('processing syslog');
         logHolder.syslog = syslogSummary.processLogLines(logHolder.syslog,
@@ -76,7 +80,8 @@
         displayLogSummary(logSummary);
         displayServiceSummary(logSummary);
       }
-      if (logHolder.syslog || logHolder.netlog || logHolder.brillolog) {
+      if (logHolder.syslog || logHolder.netlog ||
+          logHolder.brillolog || logHolder.androidlog) {
         displayLog(logHolder);
       } else {
         displayError();
@@ -426,6 +431,12 @@
   if (logHolder.syslog)
     sysLength = logHolder.syslog.length;
 
+  if (logHolder.androidlog) {
+    console.log('attempting to display an android log');
+    netLength = logHolder.androidlog.length;
+    logHolder.netlog = logHolder.androidlog;
+  }
+
   while (netLength > 0 || sysLength > 0) {
     if (netLength == 0) {
       mergedLog.appendLog(logHolder.syslog);
diff --git a/service_states.html b/service_states.html
index d5fbead..b27f907 100644
--- a/service_states.html
+++ b/service_states.html
@@ -13,6 +13,7 @@
     <script src="service.js"></script>
     <script src="process_log.js"></script>
     <script src="syslog_summary.js"></script>
+    <script src="androidlog_summary.js"></script>
   </head>
   <body>
     <h1>ChromeOS Network Log Processor</h1>
diff --git a/service_summary.css b/service_summary.css
index c9a9258..48daa82 100644
--- a/service_summary.css
+++ b/service_summary.css
@@ -12,6 +12,7 @@
 .netlog { background-color: #b0c4de; }
 .syslog { background-color: #663399; }
 .brillo { background-color: #b0deca; }
+.android { background-color: #90FF90; }
 .netevt { background-color: steelblue; }
 
 .legend {
diff --git a/syslog_summary.js b/syslog_summary.js
index 35f68d5..3d94366 100644
--- a/syslog_summary.js
+++ b/syslog_summary.js
@@ -31,7 +31,7 @@
 syslogSummary.KERNEL_INFO = / kernel: \[/;
 
 /**
- * Array of Objects holding regexs and handlers for syslog subsuctions
+ * Array of Objects holding regexs and handlers for syslog subsections
  * @type {Object[]}
  * @return {string} handler functions return log line tag label
  */
diff --git a/test/spec/LogHelperSpec.js b/test/spec/LogHelperSpec.js
index 8960685..a52f379 100644
--- a/test/spec/LogHelperSpec.js
+++ b/test/spec/LogHelperSpec.js
@@ -80,6 +80,45 @@
     expect(logHolder.fileType).toEqual('unknown');
   });
 
+  it('should detect an android bug report file', function() {
+    var androidBugReport = [
+        '========================================================',
+        '== dumpstate: 2015-11-20 12:11:35',
+        '========================================================',
+        '',
+        'Build: bullhead-userdebug 6.0.1 MMB29B 2387154 dev-keys',
+        '11-20 12:08:02.926   903  2909 D ConnectivityService: ' +
+            ' sendStickyBroadcast: action=android.net.conn.CONNECTIVITY_CHANGE',
+        '--------- beginning of main',
+        '11-20 12:08:06.463   489   897 D CommandListener: Setting iface cfg',
+        '11-20 12:08:06.521   903  2909 D ConnectivityService: Adding iface ' +
+            'wlan0 to network 169',
+        '[logcat: 0.530s elapsed]',
+        '',
+        '------ EVENT LOG (logcat -b events -v threadtime -d *:v) -----',
+        '11-20 12:01:45.958  3432  9804 I gtalk_conn_close: [276,8]'
+    ];
+    var logHolder = logHelper.detectFileType(androidBugReport);
+    expect(logHolder.fileType).toEqual('android_log');
+  });
+
+  it('should detect an android logcat file', function() {
+    var androidBugReport = [
+        '',
+        '--------- beginning of system',
+        '03-13 07:44:33.593   345   345 I vold    : ' +
+            'Vold 3.0 (the awakening) firing up',
+        '11-25 14:34:38.797  6323  6628 V KeyValueBackupJob: ' +
+            'Scheduling k/v pass in 247 minutes',
+        '--------- beginning of main',
+        '11-20 12:08:06.463   489   897 D CommandListener: Setting iface cfg',
+        '11-20 12:08:06.521   903  2909 D ConnectivityService: Adding iface ' +
+            'wlan0 to network 169'
+    ];
+    var logHolder = logHelper.detectFileType(androidBugReport);
+    expect(logHolder.fileType).toEqual('android_log');
+  });
+
   it('should properly determine log offsets', function() {
     var time1 = '2015-01-06T23:33:03.478905-08:00';
     var time2 = '2015-01-06T23:33:06.680764-08:30';