listnr_content_script.js and more: Open Tool from Listn directly

Extends this extension to inject code into listnr to support
opening this tool directly.

background.js: the "middle man" code to handle the request, launch the
tool, and pass in the feedback URL.

listnr_content_script.js: code injected into listnr.

manifest.json: New permissions to support this

popup.js: Code in the tool that can receive the feedback URL from
background.js. If tool is launched normally, this is dormant.

BUG=b:150216364
TEST=Manually tested in browser

Change-Id: Ibb34c8718e567369c7661b77d68c19a48f73ed4d
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..8250773
--- /dev/null
+++ b/background.js
@@ -0,0 +1,30 @@
+'use strict';
+
+// This code listens for a message from listnr and pops open the
+// viewer.
+
+chrome.runtime.onMessage.addListener(
+  (request, sender, sendResponse) => {
+    switch (request.action) {
+    case 'open_shill_page':
+      chrome.tabs.create(
+        {'url': chrome.extension.getURL(
+          'service_states.html#fromFeedback=true')},
+        (tab) => {
+          // Indicates the browser that actions have completed. We
+          // still wait a bit just to be safe.
+          setTimeout(() => {
+            chrome.tabs.sendMessage(tab.id, {'logData': request.data}, null,
+                                    (resp) => {
+                                      console.log('got response:');
+                                      console.log(resp);
+                                    });
+            console.log('sending response');
+            sendResponse();
+            console.log('sent response');
+          }, 500);
+        });
+    }
+    return true;
+  }
+);
diff --git a/listnr_content_script.js b/listnr_content_script.js
new file mode 100644
index 0000000..5e5bf47
--- /dev/null
+++ b/listnr_content_script.js
@@ -0,0 +1,151 @@
+'use strict';
+
+// Concept heavily borrowed from
+// //depot/google3/wireless/android/tools/android_bug_tool/extension/listnr_content_script/listnr_content_script.ts
+
+// WARNING: This function may not reference any non-globals outside this
+//          function as it is stringified below to be inserted into the page.
+function setup() {
+  if (typeof angular !== 'undefined') {
+    angular.element(document.documentElement)['injector']().invoke(
+      ($rootScope) => {
+        $rootScope.$on('$locationChangeSuccess', (e) => {
+          window.postMessage(
+            {source: '__shill_log_tool', message: 'LOCATION_UPDATED'},
+            '*');
+        });
+        $rootScope.$watch(() => {
+          window.postMessage(
+            {source: '__shill_log_tool', message: 'DIGEST'}, '*');
+        });
+      });
+  }
+}
+
+const INJECTED_ANGULAR_UPDATE_DETECTION_SCRIPT =
+  '(' + setup.toString() + ')();';
+
+// Inject script into Listnr page.
+const script = document.createElement('script');
+script.textContent = INJECTED_ANGULAR_UPDATE_DETECTION_SCRIPT;
+document.head.appendChild(script);
+
+// Setup listener for events angular update events from Listnr page.
+window.addEventListener('message', (e) => {
+  if (e.data.source === '__shill_log_tool') {
+    if (e.data.message === 'LOCATION_UPDATED' || e.data.message === 'DIGEST') {
+      delayedUpdate();
+    }
+  }
+});
+
+const UPDATE_DEBOUNCE_DELAY = 500;
+let pendingTimer = null;
+/**
+ * Schedule a delayed update. If an update is already scheduled then it is
+ * canceled and replaced with the new timer.
+ */
+function delayedUpdate() {
+  if (pendingTimer) {
+    clearTimeout(pendingTimer);
+    pendingTimer = null;
+  }
+  pendingTimer = window.setTimeout(() => {
+    pendingTimer = null;
+    update();
+  }, UPDATE_DEBOUNCE_DELAY);
+}
+
+function forEachUnprocessedAttachmentSectionsMarkAnd(callback) {
+  const attachmentSections = document.body.querySelectorAll(
+    '[section-title="Product Specific Binary Data"] > div.zipper > ng-transclude');
+  for (let i = 0; i < attachmentSections.length; ++i) {
+    const attachmentsSection =
+          attachmentSections[i];
+    if (attachmentsSection.__shill_log_tool) {
+      continue;
+    }
+    attachmentsSection.__shill_log_tool = true;
+    callback(attachmentsSection);
+  }
+}
+
+function createAttachmentLinkWithLoading(
+  text, addedClass, target) {
+  const link = document.createElement('a');
+  link.classList.add(addedClass);
+  link.style.cursor = 'pointer';
+  link.style.marginLeft = '1em';
+  link.onclick = (ev) => {
+    // Change the link to a loading stage.
+    link.style.pointerEvents = 'none';
+    if (link.lastChild == null) {
+      throw new Error('No element found in link');
+    } else {
+      link.lastChild.textContent = '  Opening...';
+    }
+
+    // Perform the action.
+    target(ev);
+  };
+  link.appendChild(document.createTextNode('  ' + text));
+  return link;
+}
+
+function removeLoadingState(element, originalText, addedClass) {
+  const link = element.querySelector('.' + addedClass);
+  if (!link) return;
+  link.style.pointerEvents = 'auto';
+  if (link.lastChild != null) {
+    link.lastChild.textContent = originalText;
+  }
+}
+
+function createOpenShillToolLink(downloadUrl, attachment) {
+  const className = 'open-shill-tool-link';
+  const magnifyingGlassEmoji = '\u{1F50D}';
+  const humanName = magnifyingGlassEmoji + ' Open Shill Log Tool';
+
+  return createAttachmentLinkWithLoading(
+    humanName, className, () => {
+      chrome.runtime.sendMessage(
+        {'action': 'open_shill_page', 'data': downloadUrl}, () => {
+          console.log('got fix link callback');
+          removeLoadingState(
+            attachment, humanName, className);
+        });
+    });
+}
+
+function getDownloadUrlFromAttachmentElement(attachment) {
+  const existingLinks = attachment.querySelectorAll('a');
+  for (let i = 0; i < existingLinks.length; ++i) {
+    const existingLink = existingLinks[i];
+    if (existingLink.textContent &&
+        existingLink.textContent.trim() === 'file_download') {
+      return existingLink.href;
+    }
+  }
+  return null;
+}
+
+function update() {
+  forEachUnprocessedAttachmentSectionsMarkAnd((attachmentsSection) => {
+    const attachments = attachmentsSection.querySelectorAll('div > div');
+    const downloadUrls = [];
+
+    for (let i = 0; i < attachments.length; ++i) {
+      const attachment = attachments[i];
+      const downloadUrl = getDownloadUrlFromAttachmentElement(attachment);
+
+      if (downloadUrl) {
+        if (downloadUrl.includes('system_logs.zip')) {
+          attachment.appendChild(
+            createOpenShillToolLink(downloadUrl, attachment));
+        }
+      }
+    }
+  });
+}
+
+delayedUpdate();
diff --git a/manifest.json b/manifest.json
index 3e03f43..673680e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -7,5 +7,23 @@
   "options_page": "service_states.html",
   "icons": {
     "128": "wifi_icon.png"
-  }
+  },
+  "background": {
+    "scripts": ["background.js"],
+    "persistent": false
+  },
+  "content_scripts": [
+    {
+      "matches": [
+        "https://listnr.corp.google.com/*",
+        "https://listnr-test.corp.google.com/*",
+        "https://listnrcrash.corp.google.com/*"
+      ],
+      "js": ["listnr_content_script.js"],
+      "run_at": "document_end"
+    }
+  ],
+  "permissions": [
+    "https://binary-feedback.googleusercontent.com/"
+  ]
 }
diff --git a/popup.js b/popup.js
new file mode 100644
index 0000000..6ef7940
--- /dev/null
+++ b/popup.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var log = null;
+var dom_loaded = false;
+
+async function downloadLog() {
+  if (log == null || dom_loaded == false)
+    return;
+  let response = await fetch(log);
+  let blob = await response.blob();
+  readFile(blob);
+};
+
+chrome.runtime.onMessage.addListener(async function(request,sender,response) {
+  log = request.logData;
+  await downloadLog();
+  response('ok');
+});
+
+window.addEventListener('DOMContentLoaded', () => {
+  if (window.location.hash == '#fromFeedback=true') {
+    // Hide the text to open a new log.
+    document.getElementById('file-load-instructions').innerHTML =
+      'loading log from feedback system...';
+  }
+  dom_loaded = true;
+  downloadLog();
+});
diff --git a/service_states.html b/service_states.html
index 6002937..94e3412 100644
--- a/service_states.html
+++ b/service_states.html
@@ -18,6 +18,7 @@
     <script src="androidlog_summary.js"></script>
     <script src="wifi_state_machine.js"></script>
     <script src="android_state_graph.js"></script>
+    <script src="popup.js"></script>
   </head>
   <body>
     <h1>Chrome OS, Brillo and Android Network Log Processor</h1>