Last active
October 20, 2017 06:27
-
-
Save lionello/3c85cada6d892f0f1124dbc8ac306b35 to your computer and use it in GitHub Desktop.
Create PlantUML/graphviz dot activity diagram for an Android project
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env dmd -run | |
import std.xml; | |
import std.file; | |
import std.stdio; | |
Element getElementByTagName(Element parent, string name) { | |
foreach (child; parent.elements) { | |
if (child.tag.name == name) { | |
return child; | |
} | |
} | |
return null; | |
} | |
string relative(string name, string packageName) pure { | |
if (name.length > packageName.length && name[0..packageName.length] == packageName) { | |
return name[1+packageName.length..$]; | |
} | |
return name; | |
} | |
string absolute(string name, string packageName) pure { | |
if (name[0] == '.') { | |
return packageName ~ name; | |
} | |
return name; | |
} | |
enum EdgeType { | |
launcher, | |
action, | |
parent, | |
intent, | |
fragment, | |
} | |
struct Edge { | |
EdgeType type; | |
string to; | |
} | |
class Node { | |
string className; | |
Edge[] edges; | |
} | |
alias Graph = Node[string]; | |
Node getNode(ref Graph graph, string node) pure { | |
auto n = node in graph; | |
return n ? *n : (graph[node] = new Node); | |
} | |
enum ACTION_NODE = "android.intent.action"; | |
string processActivity(ref Graph graph, Element c, string packageName) { | |
immutable activityName = c.tag.attr["android:name"].absolute(packageName); | |
if (activityName.length <= 8 ||activityName[$-8..$] != "Activity") { | |
writeln("Warning: activity has unusual name: "~activityName); | |
} | |
graph.getNode(activityName).className = activityName; | |
auto parentActivity = c.tag.attr.get("android:parentActivityName", null); | |
if (Element intentFilter = c.getElementByTagName("intent-filter")) { | |
if (Element action = intentFilter.getElementByTagName("action")) { | |
auto edgeType = EdgeType.action; | |
immutable actionName = action.tag.attr["android:name"]; | |
if (actionName == "android.intent.action.MAIN") { | |
auto category = intentFilter.getElementByTagName("category"); | |
assert(category.tag.attr["android:name"] == "android.intent.category.LAUNCHER"); | |
//writeln(" Entry"); | |
edgeType = EdgeType.launcher; | |
} | |
else if (actionName == "android.intent.action.VIEW") { | |
//writeln(" Linkable"); | |
} | |
else { | |
//writeln(" UNKNOWN ACTION "~actionName); | |
} | |
graph.getNode(ACTION_NODE).edges ~= Edge(edgeType, activityName); | |
} | |
} | |
if (Element metaData = c.getElementByTagName("meta-data")) { | |
if (metaData.tag.attr["android:name"] == "android.support.PARENT_ACTIVITY") { | |
immutable parentActivity2 = metaData.tag.attr["android:value"]; | |
assert(parentActivity != null, "Missing android:parentActivity attribute on "~activityName); | |
assert(parentActivity.absolute(packageName) == parentActivity2.absolute(packageName), "android:parentActivity differs from meta-data on "~activityName); | |
parentActivity = parentActivity2; | |
} | |
} | |
if (parentActivity) { | |
graph.getNode(activityName).edges ~= Edge(EdgeType.parent, parentActivity.absolute(packageName)); | |
} | |
return activityName; | |
} | |
int main(string[] args) { | |
if (args.length > 1 && (args[1] == "--help" || args[1] == "-h")) { | |
writefln("Usage: %s [path/to/AndroidManifest.xml [base_package_name]]", args[0]); | |
return 1; | |
} | |
Graph graph; | |
immutable manifestPath = args.length > 1 ? args[1] : "AndroidManifest.xml"; | |
auto manifest = new Document(cast(string) std.file.read(manifestPath)); | |
assert(manifest.tag.name == "manifest", "Root must be 'manifest'; not an Android manifest file?"); | |
immutable packageName = args.length > 2 ? args[2] : manifest.tag.attr["package"]; | |
// Find all activities | |
foreach (c; manifest.getElementByTagName("application").elements) { | |
if (c.tag.name == "activity") { | |
processActivity(graph, c, packageName); | |
} | |
} | |
import std.regex; | |
enum JavaName = `\b(?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)*[a-zA-Z_$][a-zA-Z0-9_$]*`; | |
enum NoComment = `^(?:[^\/]*?|\/[^\/])*?`; | |
auto classRegex = ctRegex!(NoComment~`(`~JavaName~`Activity)\.class\b`, "m"); | |
auto fragmentRegex = ctRegex!(NoComment~`\bnew\s+(`~JavaName~`Fragment)\s*\(`, "m"); | |
import std.path; | |
immutable javaPath = dirName(manifestPath) ~ "/java/"; | |
void visitNode(Node node) { | |
import std.string; | |
immutable classPath = node.className.replace(".", "/") ~ ".java"; | |
immutable filePackage = dirName(classPath).replace("/", "."); | |
//writeln(classPath); | |
string java = cast(string) std.file.read(javaPath ~ classPath); | |
string makeAbs(string className) { | |
auto regex = regex(`import ((?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)*`~className~")"); | |
if (auto match = java.matchFirst(regex)) { | |
return match[1]; | |
} | |
else { | |
return filePackage ~ "." ~ className.relative(packageName); | |
} | |
} | |
foreach (m; java.matchAll(classRegex)) { | |
string activityName = makeAbs(m[1]); | |
node.edges ~= Edge(EdgeType.intent, activityName); | |
} | |
foreach (m; java.matchAll(fragmentRegex)) { | |
string fragmentName = makeAbs(m[1]); | |
node.edges ~= Edge(EdgeType.fragment, fragmentName); | |
// Recurse into fragment (only first time) | |
Node fragmentNode = graph.getNode(fragmentName); | |
if (fragmentNode.className is null) { | |
fragmentNode.className = fragmentName; | |
visitNode(fragmentNode); | |
} | |
} | |
} | |
foreach (name, node; graph) { | |
if (node.className !is null) { | |
assert(node.className == name, node.className~" not equal to "~name); | |
visitNode(node); | |
} | |
} | |
version(UML) { | |
writeln("@startuml"); | |
foreach (id, node; graph) { | |
if (id == ACTION_NODE) { | |
id = "(*)"; | |
} | |
else { | |
id = id.relative(packageName); | |
} | |
bool[string] already; | |
foreach(edge; node.edges) { | |
if (edge.to in already) { | |
continue; | |
} | |
already[edge.to] = true; | |
string dir = edge.type == EdgeType.parent ? "up" : ""; | |
writeln(id ~ " -"~dir~"-> " ~ edge.to.relative(packageName)); | |
} | |
} | |
writeln("@enduml"); | |
} | |
else { | |
writeln("@startdot"); | |
writeln("digraph G {"); | |
foreach (id, node; graph) { | |
if (id == ACTION_NODE) { | |
id = `"(*)"`; | |
} | |
else { | |
id = '"' ~ id.relative(packageName) ~ '"'; | |
} | |
bool[string] already; | |
foreach(edge; node.edges) { | |
if (edge.to in already) { | |
continue; | |
} | |
already[edge.to] = true; | |
writeln(id ~ ` -> "` ~ edge.to.relative(packageName) ~ '"'); | |
} | |
} | |
writeln("}"); | |
writeln("@enddot"); | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment