summaryrefslogtreecommitdiff
path: root/android/view/ViewHierarchyEncoder.java
blob: 87702161070b5422df8e2348764f5ffacb8afd24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package android.view;

import android.annotation.NonNull;
import android.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

/**
 * {@link ViewHierarchyEncoder} is a serializer that is tailored towards writing out
 * view hierarchies (the view tree, along with the properties for each view) to a stream.
 *
 * It is typically used as follows:
 * <pre>
 *   ViewHierarchyEncoder e = new ViewHierarchyEncoder();
 *
 *   for (View view : views) {
 *      e.beginObject(view);
 *      e.addProperty("prop1", value);
 *      ...
 *      e.endObject();
 *   }
 *
 *   // repeat above snippet for each view, finally end with:
 *   e.endStream();
 * </pre>
 *
 * <p>On the stream, a snippet such as the above gets encoded as a series of Map's (one
 * corresponding to each view) with the property name as the key and the property value
 * as the value.
 *
 * <p>Since the property names are practically the same across all views, rather than using
 * the property name directly as the key, we use a short integer id corresponding to each
 * property name as the key. A final map is added at the end which contains the mapping
 * from the integer to its property name.
 *
 * <p>A value is encoded as a single byte type identifier followed by the encoding of the
 * value. Only primitive types are supported as values, in addition to the Map type.
 *
 * @hide
 */
public class ViewHierarchyEncoder {
    // Prefixes for simple primitives. These match the JNI definitions.
    private static final byte SIG_BOOLEAN = 'Z';
    private static final byte SIG_BYTE = 'B';
    private static final byte SIG_SHORT = 'S';
    private static final byte SIG_INT = 'I';
    private static final byte SIG_LONG = 'J';
    private static final byte SIG_FLOAT = 'F';
    private static final byte SIG_DOUBLE = 'D';

    // Prefixes for some commonly used objects
    private static final byte SIG_STRING = 'R';

    private static final byte SIG_MAP = 'M'; // a map with an short key
    private static final short SIG_END_MAP = 0;

    private final DataOutputStream mStream;

    private final Map<String,Short> mPropertyNames = new HashMap<String, Short>(200);
    private short mPropertyId = 1;
    private Charset mCharset = Charset.forName("utf-8");

    public ViewHierarchyEncoder(@NonNull ByteArrayOutputStream stream) {
        mStream = new DataOutputStream(stream);
    }

    public void beginObject(@NonNull Object o) {
        startPropertyMap();
        addProperty("meta:__name__", o.getClass().getName());
        addProperty("meta:__hash__", o.hashCode());
    }

    public void endObject() {
        endPropertyMap();
    }

    public void endStream() {
        // write out the string table
        startPropertyMap();
        addProperty("__name__", "propertyIndex");
        for (Map.Entry<String,Short> entry : mPropertyNames.entrySet()) {
            writeShort(entry.getValue());
            writeString(entry.getKey());
        }
        endPropertyMap();
    }

    public void addProperty(@NonNull String name, boolean v) {
        writeShort(createPropertyIndex(name));
        writeBoolean(v);
    }

    public void addProperty(@NonNull String name, short s) {
        writeShort(createPropertyIndex(name));
        writeShort(s);
    }

    public void addProperty(@NonNull String name, int v) {
        writeShort(createPropertyIndex(name));
        writeInt(v);
    }

    public void addProperty(@NonNull String name, float v) {
        writeShort(createPropertyIndex(name));
        writeFloat(v);
    }

    public void addProperty(@NonNull String name, @Nullable String s) {
        writeShort(createPropertyIndex(name));
        writeString(s);
    }

    /**
     * Writes the given name as the property name, and leaves it to the callee
     * to fill in value for this property.
     */
    public void addPropertyKey(@NonNull String name) {
        writeShort(createPropertyIndex(name));
    }

    private short createPropertyIndex(@NonNull String name) {
        Short index = mPropertyNames.get(name);
        if (index == null) {
            index = mPropertyId++;
            mPropertyNames.put(name, index);
        }

        return index;
    }

    private void startPropertyMap() {
        try {
            mStream.write(SIG_MAP);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }

    private void endPropertyMap() {
        writeShort(SIG_END_MAP);
    }

    private void writeBoolean(boolean v) {
        try {
            mStream.write(SIG_BOOLEAN);
            mStream.write(v ? 1 : 0);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }

    private void writeShort(short s) {
        try {
            mStream.write(SIG_SHORT);
            mStream.writeShort(s);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }

    private void writeInt(int i) {
        try {
            mStream.write(SIG_INT);
            mStream.writeInt(i);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }

    private void writeFloat(float v) {
        try {
            mStream.write(SIG_FLOAT);
            mStream.writeFloat(v);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }

    private void writeString(@Nullable String s) {
        if (s == null) {
            s = "";
        }

        try {
            mStream.write(SIG_STRING);
            byte[] bytes = s.getBytes(mCharset);

            short len = (short)Math.min(bytes.length, Short.MAX_VALUE);
            mStream.writeShort(len);

            mStream.write(bytes, 0, len);
        } catch (IOException e) {
            // does not happen since the stream simply wraps a ByteArrayOutputStream
        }
    }
}