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 } } }