Skip to content

Instantly share code, notes, and snippets.

@lionello
Last active Oct 20, 2017
Embed
What would you like to do?
Create PlantUML/graphviz dot activity diagram for an Android project
#!/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