1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// This class provides functions to export a FilterGraph.
18
19package androidx.media.filterfw;
20
21import android.content.Context;
22
23import java.io.FileOutputStream;
24import java.io.OutputStreamWriter;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.Map.Entry;
28import java.util.Set;
29
30/**
31 * This class provides functions to export a FilterGraph as a DOT file.
32 */
33public class GraphExporter {
34
35    /**
36     * Exports the graph as DOT (see http://en.wikipedia.org/wiki/DOT_language).
37     * Using the exported file, the graph can be visualized e.g. with the command line tool dot.
38     * Optionally, one may /exclude/ unconnected optional ports (third parameter = false),
39     * since they can quickly clutter the visualization (and, depending on the purpose, may not
40     * be interesting).
41     *
42     * Example workflow:
43     *  1. run application on device, make sure it calls exportGraphAsDOT(...);
44     *  2. adb pull /data/data/<application name>/files/<graph filename>.gv graph.gv
45     *  3. dot -Tpng graph.gv -o graph.png
46     *  4. eog graph.png
47     */
48    static public void exportAsDot(FilterGraph graph, String filename,
49            boolean includeUnconnectedOptionalPorts)
50            throws java.io.FileNotFoundException, java.io.IOException {
51        // Initialize, open file stream
52        Context myAppContext = graph.getContext().getApplicationContext();
53        Filter[] filters = graph.getAllFilters();
54        FileOutputStream fOut = myAppContext.openFileOutput(filename, Context.MODE_PRIVATE);
55        OutputStreamWriter dotFile = new OutputStreamWriter(fOut);
56
57        // Write beginning of DOT file
58        dotFile.write("digraph graphname {\n");
59        dotFile.write("  node [shape=record];\n");
60
61        // N.B. For specification and lots of examples of the DOT language, see
62        //   http://www.graphviz.org/Documentation/dotguide.pdf
63
64        // Iterate over all filters of the graph, write corresponding DOT node elements
65
66        for(Filter filter : filters) {
67            dotFile.write(getDotName("  " + filter.getName()) + " [label=\"{");
68
69            // Write upper part of element (i.e., input ports)
70            Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts);
71            if(inputPorts.size() > 0) {
72                dotFile.write(" { ");
73                int counter = 0;
74                for(String p : inputPorts) {
75                    dotFile.write("<" + getDotName(p) + "_IN>" + p);
76                    if(++counter != inputPorts.size()) dotFile.write(" | ");
77                }
78                dotFile.write(" } | ");
79            }
80
81            // Write center part of element (i.e., element label)
82            dotFile.write(filter.getName());
83
84            // Write lower part of element (i.e., output ports)
85            Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts);
86            if(outputPorts.size() > 0) {
87                dotFile.write(" | { ");
88                int counter = 0;
89                for(String p : outputPorts) {
90                    dotFile.write("<" + getDotName(p) + "_OUT>" + p);
91                    if(++counter != outputPorts.size()) dotFile.write(" | ");
92                }
93                dotFile.write(" } ");
94            }
95
96            dotFile.write("}\"];\n");
97        }
98        dotFile.write("\n");
99
100        // Iterate over all filters again to collect connections and find unconnected ports
101
102        int dummyNodeCounter = 0;
103        for(Filter filter : filters) {
104            Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts);
105            for(String portName : outputPorts) {
106                OutputPort source = filter.getConnectedOutputPort(portName);
107                if(source != null) {
108                    // Found a connection, draw it
109                    InputPort target = source.getTarget();
110                    dotFile.write("  " +
111                        getDotName(source.getFilter().getName()) + ":" +
112                        getDotName(source.getName()) + "_OUT -> " +
113                        getDotName(target.getFilter().getName()) + ":" +
114                        getDotName(target.getName()) + "_IN;\n" );
115                } else {
116                    // Found a unconnected output port, add dummy node
117                    String color = filter.getSignature().getOutputPortInfo(portName).isRequired()
118                        ? "red" : "blue";  // red for unconnected, required ports
119                    dotFile.write("  " +
120                        "dummy" + (++dummyNodeCounter) +
121                        " [shape=point,label=\"\",color=" + color + "];\n" +
122                        "  " + getDotName(filter.getName()) + ":" +
123                        getDotName(portName) + "_OUT -> " +
124                        "dummy" + dummyNodeCounter + " [color=" + color + "];\n");
125                }
126            }
127
128            Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts);
129            for(String portName : inputPorts) {
130                InputPort target = filter.getConnectedInputPort(portName);
131                if(target != null) {
132                    // Found a connection -- nothing to do, connections have been written out above
133                } else {
134                    // Found a unconnected input port, add dummy node
135                    String color = filter.getSignature().getInputPortInfo(portName).isRequired()
136                        ? "red" : "blue";  // red for unconnected, required ports
137                    dotFile.write("  " +
138                        "dummy" + (++dummyNodeCounter) +
139                        " [shape=point,label=\"\",color=" + color + "];\n" +
140                        "  dummy" + dummyNodeCounter + " -> " +
141                        getDotName(filter.getName()) + ":" +
142                        getDotName(portName) + "_IN [color=" + color + "];\n");
143                }
144            }
145        }
146
147        // Write end of DOT file, close file stream
148        dotFile.write("}\n");
149        dotFile.flush();
150        dotFile.close();
151    }
152
153    // Internal methods
154
155    // From element's name in XML, create DOT-allowed element name
156    static private String getDotName(String raw) {
157        return raw.replaceAll("\\.", "___"); // DOT does not allow . in element names
158    }
159
160    // Retrieve all input ports of a filter, including:
161    //  unconnected ports (which can not be retrieved from the filter, only from the signature), and
162    //  additional (connected) ports not listed in the signature (which is allowed by default,
163    //    unless disallowOtherInputs is defined in signature).
164    // With second parameter = false, *omit* unconnected optional ports.
165    static private Set<String> getInputPorts(Filter filter, boolean includeUnconnectedOptional) {
166        // add (connected) ports from filter
167        Set<String> ports = new HashSet<String>();
168        ports.addAll(filter.getConnectedInputPortMap().keySet());
169
170        // add (unconnected) ports from signature
171        HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getInputPorts();
172        if(signaturePorts != null){
173            for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) {
174                if(includeUnconnectedOptional || e.getValue().isRequired()) {
175                    ports.add(e.getKey());
176                }
177            }
178        }
179        return ports;
180    }
181
182    // Retrieve all output ports of a filter (analogous to above function)
183    static private Set<String> getOutputPorts(Filter filter, boolean includeUnconnectedOptional) {
184        // add (connected) ports from filter
185        Set<String> ports = new HashSet<String>();
186        ports.addAll(filter.getConnectedOutputPortMap().keySet());
187
188        // add (unconnected) ports from signature
189        HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getOutputPorts();
190        if(signaturePorts != null){
191            for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) {
192                if(includeUnconnectedOptional || e.getValue().isRequired()) {
193                    ports.add(e.getKey());
194                }
195            }
196        }
197        return ports;
198    }
199}
200