Support parsing class graphs in Android dependency visualization

The index page has been forked into class/package versions, which are
identical for now but expected to differ more in the future. Although
there is no way to move between class/package graphs via UI controls as
of yet, it is possible to do so by changing the URL manually (ie., go to
class_index.html instead of package_index.html). The UI control to
generate the current URL also works for class graphs.

Bug: 1093962
Change-Id: I725c9107eb04388ef8228b63dc4ec8c6aa28d9e0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2285327
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: Samuel Huang <huangs@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#786400}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 3288fafe1126fc1be1f92e667e9c6c6426cf1961
diff --git a/dependency_analysis/js/chrome_hooks.js b/dependency_analysis/js/chrome_hooks.js
index 68e1de0..b4b845b 100644
--- a/dependency_analysis/js/chrome_hooks.js
+++ b/dependency_analysis/js/chrome_hooks.js
@@ -13,6 +13,16 @@
       .replace('chrome.browser.', 'c.b.');
 }
 
+/**
+ * Shortens a class name to be displayed in the svg.
+ * @param {string} name The full class name to shorten.
+ * @return {string} The shortened class name.
+ */
+function shortenClassName(name) {
+  return name.substring(name.lastIndexOf('.') + 1);
+}
+
 export {
   shortenPackageName,
+  shortenClassName,
 };
diff --git a/dependency_analysis/js/class_index.html b/dependency_analysis/js/class_index.html
new file mode 100644
index 0000000..837ffbd
--- /dev/null
+++ b/dependency_analysis/js/class_index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <link rel="stylesheet" type="text/css" href="./common.css"></link>
+    <script type="text/javascript" src="./node_modules/d3/dist/d3.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+  </head>
+  <body>
+    <div id="class-graph-page"></div>
+    <script type="module" src="./class_index.js"></script>
+  </body>
+</html>
diff --git a/dependency_analysis/js/class_index.js b/dependency_analysis/js/class_index.js
new file mode 100644
index 0000000..c9ca4f5
--- /dev/null
+++ b/dependency_analysis/js/class_index.js
@@ -0,0 +1,23 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ClassGraphPage} from './vue_components/class_graph_page.js';
+
+// For ease of development, we currently serve all our JSON and other assets
+// through a simple Python server at localhost:8888. This should be changed
+// as we find other ways to serve the assets (user upload or hosted externally).
+const LOCALHOST = 'http://localhost:8888';
+
+// TODO(yjlong): Currently we take JSON served by a Python server running on
+// the side. Replace this with a user upload or pull from some other source.
+document.addEventListener('DOMContentLoaded', () => {
+  d3.json(`${LOCALHOST}/json_graph.txt`).then(data => {
+    new ClassGraphPage({
+      el: '#class-graph-page',
+      propsData: {
+        graphJson: data.class_graph,
+      },
+    });
+  });
+});
diff --git a/dependency_analysis/js/index.css b/dependency_analysis/js/common.css
similarity index 100%
rename from dependency_analysis/js/index.css
rename to dependency_analysis/js/common.css
diff --git a/dependency_analysis/js/index.html b/dependency_analysis/js/package_index.html
similarity index 67%
rename from dependency_analysis/js/index.html
rename to dependency_analysis/js/package_index.html
index ae36c36..cad3283 100644
--- a/dependency_analysis/js/index.html
+++ b/dependency_analysis/js/package_index.html
@@ -1,13 +1,12 @@
 <!DOCTYPE html>
 <html lang="en">
   <head>
-    <link rel="stylesheet" type="text/css" href="./index.css"></link>
+    <link rel="stylesheet" type="text/css" href="./common.css"></link>
     <script type="text/javascript" src="./node_modules/d3/dist/d3.js"></script>
     <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
   </head>
   <body>
     <div id="package-graph-page"></div>
-    <script type="module" src="./index.js"></script>
+    <script type="module" src="./package_index.js"></script>
   </body>
 </html>
-
diff --git a/dependency_analysis/js/index.js b/dependency_analysis/js/package_index.js
similarity index 100%
rename from dependency_analysis/js/index.js
rename to dependency_analysis/js/package_index.js
diff --git a/dependency_analysis/js/process_graph_json.js b/dependency_analysis/js/process_graph_json.js
index 1df0e73..a7c0f51 100644
--- a/dependency_analysis/js/process_graph_json.js
+++ b/dependency_analysis/js/process_graph_json.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import {Node, GraphModel} from './graph_model.js';
-import {shortenPackageName} from './chrome_hooks.js';
+import {shortenPackageName, shortenClassName} from './chrome_hooks.js';
 
 /**
  * A graph read from JSON.
@@ -13,15 +13,23 @@
  */
 
 /**
+ * A function that shortens a full Java name for display in the visualization.
+ * @callback NameShortener
+ * @param {string} name The name to be shortened.
+ * @return {string} The shortened name.
+ */
+
+/**
  * Transforms a graph JSON generated by Python scripts
  * (generate_json_dependency_graph.py) into a working format for d3.
  * @param {!JsonGraph} jsonGraph The JSON graph to parse.
+ * @param {!NameShortener} shortenName The function to shorten full names.
  * @return {!GraphModel} The parsed out GraphModel object.
  */
-function parseGraphModelFromJson(jsonGraph) {
+function parseGraphModelFromJson(jsonGraph, shortenName) {
   const graph = new GraphModel();
   for (const nodeData of jsonGraph.nodes) {
-    const node = new Node(nodeData.name, shortenPackageName(nodeData.name));
+    const node = new Node(nodeData.name, shortenName(nodeData.name));
     graph.addNodeIfNew(node);
   }
   for (const edgeData of jsonGraph.edges) {
@@ -33,6 +41,26 @@
   return graph;
 }
 
+
+/**
+ * Parses a package JSON graph generated by Python scripts.
+ * @param {!JsonGraph} jsonGraph The JSON package graph to parse.
+ * @return {!GraphModel} The parsed out GraphModel object.
+ */
+function parsePackageGraphModelFromJson(jsonGraph) {
+  return parseGraphModelFromJson(jsonGraph, shortenPackageName);
+}
+
+/**
+ * Parses a class JSON graph generated by Python scripts.
+ * @param {!JsonGraph} jsonGraph The JSON class graph to parse.
+ * @return {!GraphModel} The parsed out GraphModel object.
+ */
+function parseClassGraphModelFromJson(jsonGraph) {
+  return parseGraphModelFromJson(jsonGraph, shortenClassName);
+}
+
 export {
-  parseGraphModelFromJson,
+  parsePackageGraphModelFromJson,
+  parseClassGraphModelFromJson,
 };
diff --git a/dependency_analysis/js/vue_components/class_graph_page.js b/dependency_analysis/js/vue_components/class_graph_page.js
new file mode 100644
index 0000000..13ec369
--- /dev/null
+++ b/dependency_analysis/js/vue_components/class_graph_page.js
@@ -0,0 +1,160 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {CUSTOM_EVENTS} from '../vue_custom_events.js';
+
+import {GraphFilterInput} from './graph_filter_input.js';
+import {GraphFilterItems} from './graph_filter_items.js';
+import {GraphInboundInput} from './graph_inbound_input.js';
+import {GraphOutboundInput} from './graph_outbound_input.js';
+import {GraphVisualization} from './graph_visualization.js';
+import {GraphSelectedNodeDetails} from './graph_selected_node_details.js';
+import {PageUrlGenerator} from './page_url_generator.js';
+
+import {parseClassGraphModelFromJson} from '../process_graph_json.js';
+import {generateFilterFromUrl} from '../url_processor.js';
+import {PageModel} from '../page_model.js';
+import {Node} from '../graph_model.js';
+
+const ClassGraphPage = Vue.component('class-graph-page', {
+  components: {
+    'graph-filter-input': GraphFilterInput,
+    'graph-filter-items': GraphFilterItems,
+    'graph-inbound-input': GraphInboundInput,
+    'graph-outbound-input': GraphOutboundInput,
+    'graph-visualization': GraphVisualization,
+    'graph-selected-node-details': GraphSelectedNodeDetails,
+    'page-url-generator': PageUrlGenerator,
+  },
+  props: ['graphJson'],
+
+  /**
+   * Various references to objects used across the entire page.
+   * @typedef {Object} PageData
+   * @property {PageModel} pageModel The data store for the page.
+   * @property {number} graphDataUpdateTicker Incremented every time we want to
+   *     trigger a visualization update. See graph_visualization.js for further
+   *     explanation on this variable.
+   */
+
+  /**
+   * @return {PageData} The objects used throughout the page.
+   */
+  data: function() {
+    const graphModel = parseClassGraphModelFromJson(this.graphJson);
+    const pageModel = new PageModel(graphModel);
+
+    return {
+      pageModel,
+      graphDataUpdateTicker: 0,
+    };
+  },
+  /**
+   * Parses out data from the current URL to initialize the visualization with.
+   */
+  mounted: function() {
+    const includedNodesInUrl = generateFilterFromUrl(document.URL);
+
+    if (includedNodesInUrl.length !== 0) {
+      this.addNodesToFilter(includedNodesInUrl);
+    } else {
+      // TODO(yjlong): This is test data. Remove this when no longer needed.
+      this.addNodesToFilter([
+        'org.chromium.chrome.browser.tabmodel.AsyncTabParams',
+        'org.chromium.chrome.browser.ActivityTabProvider',
+        'org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver',
+      ]);
+    }
+
+    this.setOutboundDepth(1);
+    this.graphDataUpdateTicker++;
+  },
+  methods: {
+    /**
+     * @param {string} nodeName The node to add.
+     */
+    addNodeToFilter: function(nodeName) {
+      this.pageModel.nodeFilterData.addNode(nodeName);
+      this.graphDataUpdateTicker++;
+    },
+    /**
+     * Adds all supplied nodes to the node filter, then increments
+     * `graphDataUpdateTicker` once at the end, even if `nodeNames` is empty.
+     * @param {!Array<string>} nodeNames The nodes to add.
+     */
+    addNodesToFilter: function(nodeNames) {
+      for (const nodeName of nodeNames) {
+        this.pageModel.nodeFilterData.addNode(nodeName);
+      }
+      this.graphDataUpdateTicker++;
+    },
+    /**
+     * @param {string} nodeName The node to remove.
+     */
+    removeNodeFromFilter: function(nodeName) {
+      this.pageModel.nodeFilterData.removeNode(nodeName);
+      this.graphDataUpdateTicker++;
+    },
+    /**
+     * @param {number} depth The new inbound depth.
+     */
+    setInboundDepth: function(depth) {
+      this.pageModel.inboundDepthData.inboundDepth = depth;
+      this.graphDataUpdateTicker++;
+    },
+    /**
+     * @param {number} depth The new outbound depth.
+     */
+    setOutboundDepth: function(depth) {
+      this.pageModel.outboundDepthData.outboundDepth = depth;
+      this.graphDataUpdateTicker++;
+    },
+    /**
+     * @param {?Node} node The selected node. May be `null`, which will reset
+     *     the selection to the state with no node.
+     */
+    graphNodeClicked: function(node) {
+      this.pageModel.selectedNodeDetailsData.selectedNode = node;
+    },
+  },
+  template: `
+    <div id="page-container">
+      <div id="page-controls">
+        <graph-filter-input
+          @${CUSTOM_EVENTS.FILTER_SUBMITTED}="this.addNodeToFilter"
+        ></graph-filter-input>
+        <graph-filter-items
+          :node-filter-data="this.pageModel.nodeFilterData"
+          @${CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED}="this.removeNodeFromFilter"
+        ></graph-filter-items>
+        <graph-inbound-input
+          :inbound-depth-data="this.pageModel.inboundDepthData"
+          @${CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED}="this.setInboundDepth"
+        ></graph-inbound-input>
+        <graph-outbound-input
+          :outbound-depth-data="this.pageModel.outboundDepthData"
+          @${CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED}="this.setOutboundDepth"
+        ></graph-outbound-input>
+      </div>
+      <div id="graph-and-node-details-container">
+        <graph-visualization
+          :graph-data-update-ticker="this.graphDataUpdateTicker"
+          :page-model="this.pageModel"
+          @${CUSTOM_EVENTS.NODE_CLICKED}="graphNodeClicked"
+        ></graph-visualization>
+        <graph-selected-node-details
+          :selected-node-details-data="this.pageModel.selectedNodeDetailsData"
+          @${CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED}="addNodeToFilter"
+          @${CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED}="removeNodeFromFilter"
+        ></graph-selected-node-details>
+      </div>
+      <page-url-generator
+        :node-filter-data="this.pageModel.nodeFilterData"
+      ></page-url-generator>
+    </div>`,
+});
+
+export {
+  ClassGraphPage,
+};
diff --git a/dependency_analysis/js/vue_components/package_graph_page.js b/dependency_analysis/js/vue_components/package_graph_page.js
index 9baf6db..53f602f 100644
--- a/dependency_analysis/js/vue_components/package_graph_page.js
+++ b/dependency_analysis/js/vue_components/package_graph_page.js
@@ -12,7 +12,7 @@
 import {GraphSelectedNodeDetails} from './graph_selected_node_details.js';
 import {PageUrlGenerator} from './page_url_generator.js';
 
-import {parseGraphModelFromJson} from '../process_graph_json.js';
+import {parsePackageGraphModelFromJson} from '../process_graph_json.js';
 import {generateFilterFromUrl} from '../url_processor.js';
 import {PageModel} from '../page_model.js';
 import {Node} from '../graph_model.js';
@@ -42,7 +42,7 @@
    * @return {PageData} The objects used throughout the page.
   */
   data: function() {
-    const graphModel = parseGraphModelFromJson(this.graphJson);
+    const graphModel = parsePackageGraphModelFromJson(this.graphJson);
     const pageModel = new PageModel(graphModel);
 
     return {