1 package android.view;
2 
3 import android.annotation.NonNull;
4 import android.annotation.Nullable;
5 
6 import java.io.ByteArrayOutputStream;
7 import java.io.DataOutputStream;
8 import java.io.IOException;
9 import java.nio.charset.Charset;
10 import java.util.HashMap;
11 import java.util.Map;
12 
13 /**
14  * {@link ViewHierarchyEncoder} is a serializer that is tailored towards writing out
15  * view hierarchies (the view tree, along with the properties for each view) to a stream.
16  *
17  * It is typically used as follows:
18  * <pre>
19  *   ViewHierarchyEncoder e = new ViewHierarchyEncoder();
20  *
21  *   for (View view : views) {
22  *      e.beginObject(view);
23  *      e.addProperty("prop1", value);
24  *      ...
25  *      e.endObject();
26  *   }
27  *
28  *   // repeat above snippet for each view, finally end with:
29  *   e.endStream();
30  * </pre>
31  *
32  * <p>On the stream, a snippet such as the above gets encoded as a series of Map's (one
33  * corresponding to each view) with the property name as the key and the property value
34  * as the value.
35  *
36  * <p>Since the property names are practically the same across all views, rather than using
37  * the property name directly as the key, we use a short integer id corresponding to each
38  * property name as the key. A final map is added at the end which contains the mapping
39  * from the integer to its property name.
40  *
41  * <p>A value is encoded as a single byte type identifier followed by the encoding of the
42  * value. Only primitive types are supported as values, in addition to the Map type.
43  *
44  * @hide
45  */
46 public class ViewHierarchyEncoder {
47     // Prefixes for simple primitives. These match the JNI definitions.
48     private static final byte SIG_BOOLEAN = 'Z';
49     private static final byte SIG_BYTE = 'B';
50     private static final byte SIG_SHORT = 'S';
51     private static final byte SIG_INT = 'I';
52     private static final byte SIG_LONG = 'J';
53     private static final byte SIG_FLOAT = 'F';
54     private static final byte SIG_DOUBLE = 'D';
55 
56     // Prefixes for some commonly used objects
57     private static final byte SIG_STRING = 'R';
58 
59     private static final byte SIG_MAP = 'M'; // a map with an short key
60     private static final short SIG_END_MAP = 0;
61 
62     private final DataOutputStream mStream;
63 
64     private final Map<String,Short> mPropertyNames = new HashMap<String, Short>(200);
65     private short mPropertyId = 1;
66     private Charset mCharset = Charset.forName("utf-8");
67 
ViewHierarchyEncoder(@onNull ByteArrayOutputStream stream)68     public ViewHierarchyEncoder(@NonNull ByteArrayOutputStream stream) {
69         mStream = new DataOutputStream(stream);
70     }
71 
beginObject(@onNull Object o)72     public void beginObject(@NonNull Object o) {
73         startPropertyMap();
74         addProperty("meta:__name__", o.getClass().getName());
75         addProperty("meta:__hash__", o.hashCode());
76     }
77 
endObject()78     public void endObject() {
79         endPropertyMap();
80     }
81 
endStream()82     public void endStream() {
83         // write out the string table
84         startPropertyMap();
85         addProperty("__name__", "propertyIndex");
86         for (Map.Entry<String,Short> entry : mPropertyNames.entrySet()) {
87             writeShort(entry.getValue());
88             writeString(entry.getKey());
89         }
90         endPropertyMap();
91     }
92 
addProperty(@onNull String name, boolean v)93     public void addProperty(@NonNull String name, boolean v) {
94         writeShort(createPropertyIndex(name));
95         writeBoolean(v);
96     }
97 
addProperty(@onNull String name, short s)98     public void addProperty(@NonNull String name, short s) {
99         writeShort(createPropertyIndex(name));
100         writeShort(s);
101     }
102 
addProperty(@onNull String name, int v)103     public void addProperty(@NonNull String name, int v) {
104         writeShort(createPropertyIndex(name));
105         writeInt(v);
106     }
107 
addProperty(@onNull String name, float v)108     public void addProperty(@NonNull String name, float v) {
109         writeShort(createPropertyIndex(name));
110         writeFloat(v);
111     }
112 
addProperty(@onNull String name, @Nullable String s)113     public void addProperty(@NonNull String name, @Nullable String s) {
114         writeShort(createPropertyIndex(name));
115         writeString(s);
116     }
117 
118     /**
119      * Writes the given name as the property name, and leaves it to the callee
120      * to fill in value for this property.
121      */
addPropertyKey(@onNull String name)122     public void addPropertyKey(@NonNull String name) {
123         writeShort(createPropertyIndex(name));
124     }
125 
createPropertyIndex(@onNull String name)126     private short createPropertyIndex(@NonNull String name) {
127         Short index = mPropertyNames.get(name);
128         if (index == null) {
129             index = mPropertyId++;
130             mPropertyNames.put(name, index);
131         }
132 
133         return index;
134     }
135 
startPropertyMap()136     private void startPropertyMap() {
137         try {
138             mStream.write(SIG_MAP);
139         } catch (IOException e) {
140             // does not happen since the stream simply wraps a ByteArrayOutputStream
141         }
142     }
143 
endPropertyMap()144     private void endPropertyMap() {
145         writeShort(SIG_END_MAP);
146     }
147 
writeBoolean(boolean v)148     private void writeBoolean(boolean v) {
149         try {
150             mStream.write(SIG_BOOLEAN);
151             mStream.write(v ? 1 : 0);
152         } catch (IOException e) {
153             // does not happen since the stream simply wraps a ByteArrayOutputStream
154         }
155     }
156 
writeShort(short s)157     private void writeShort(short s) {
158         try {
159             mStream.write(SIG_SHORT);
160             mStream.writeShort(s);
161         } catch (IOException e) {
162             // does not happen since the stream simply wraps a ByteArrayOutputStream
163         }
164     }
165 
writeInt(int i)166     private void writeInt(int i) {
167         try {
168             mStream.write(SIG_INT);
169             mStream.writeInt(i);
170         } catch (IOException e) {
171             // does not happen since the stream simply wraps a ByteArrayOutputStream
172         }
173     }
174 
writeFloat(float v)175     private void writeFloat(float v) {
176         try {
177             mStream.write(SIG_FLOAT);
178             mStream.writeFloat(v);
179         } catch (IOException e) {
180             // does not happen since the stream simply wraps a ByteArrayOutputStream
181         }
182     }
183 
writeString(@ullable String s)184     private void writeString(@Nullable String s) {
185         if (s == null) {
186             s = "";
187         }
188 
189         try {
190             mStream.write(SIG_STRING);
191             byte[] bytes = s.getBytes(mCharset);
192 
193             short len = (short)Math.min(bytes.length, Short.MAX_VALUE);
194             mStream.writeShort(len);
195 
196             mStream.write(bytes, 0, len);
197         } catch (IOException e) {
198             // does not happen since the stream simply wraps a ByteArrayOutputStream
199         }
200     }
201 }
202