| package android.view; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| |
| 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"); |
| |
| private boolean mUserPropertiesEnabled = true; |
| |
| public ViewHierarchyEncoder(@NonNull ByteArrayOutputStream stream) { |
| mStream = new DataOutputStream(stream); |
| } |
| |
| public void setUserPropertiesEnabled(boolean enabled) { |
| mUserPropertiesEnabled = enabled; |
| } |
| |
| 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(); |
| } |
| |
| @UnsupportedAppUsage |
| 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); |
| } |
| |
| @UnsupportedAppUsage |
| public void addProperty(@NonNull String name, int v) { |
| writeShort(createPropertyIndex(name)); |
| writeInt(v); |
| } |
| |
| @UnsupportedAppUsage |
| public void addProperty(@NonNull String name, float v) { |
| writeShort(createPropertyIndex(name)); |
| writeFloat(v); |
| } |
| |
| @UnsupportedAppUsage |
| public void addProperty(@NonNull String name, @Nullable String s) { |
| writeShort(createPropertyIndex(name)); |
| writeString(s); |
| } |
| |
| /** |
| * Encodes a user defined property if they are allowed to be encoded |
| * |
| * @see #setUserPropertiesEnabled(boolean) |
| */ |
| public void addUserProperty(@NonNull String name, @Nullable String s) { |
| if (mUserPropertiesEnabled) { |
| addProperty(name, 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 |
| } |
| } |
| } |