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