| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package io.flutter.plugin.common; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import io.flutter.BuildConfig; |
| import io.flutter.Log; |
| import java.io.ByteArrayOutputStream; |
| import java.math.BigInteger; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| /** |
| * MessageCodec using the Flutter standard binary encoding. |
| * |
| * <p>This codec is guaranteed to be compatible with the corresponding <a |
| * href="https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html">StandardMessageCodec</a> |
| * on the Dart side. These parts of the Flutter SDK are evolved synchronously. |
| * |
| * <p>Supported messages are acyclic values of these forms: |
| * |
| * <ul> |
| * <li>null |
| * <li>Booleans |
| * <li>Bytes, Shorts, Integers, Longs |
| * <li>BigIntegers (see below) |
| * <li>Floats, Doubles |
| * <li>Strings |
| * <li>byte[], int[], long[], float[], double[] |
| * <li>Lists of supported values |
| * <li>Maps with supported keys and values |
| * </ul> |
| * |
| * <p>On the Dart side, these values are represented as follows: |
| * |
| * <ul> |
| * <li>null: null |
| * <li>Boolean: bool |
| * <li>Byte, Short, Integer, Long: int |
| * <li>Float, Double: double |
| * <li>String: String |
| * <li>byte[]: Uint8List |
| * <li>int[]: Int32List |
| * <li>long[]: Int64List |
| * <li>float[]: Float32List |
| * <li>double[]: Float64List |
| * <li>List: List |
| * <li>Map: Map |
| * </ul> |
| * |
| * <p>BigIntegers are represented in Dart as strings with the hexadecimal representation of the |
| * integer's value. |
| * |
| * <p>To extend the codec, overwrite the writeValue and readValueOfType methods. |
| */ |
| public class StandardMessageCodec implements MessageCodec<Object> { |
| private static final String TAG = "StandardMessageCodec#"; |
| public static final StandardMessageCodec INSTANCE = new StandardMessageCodec(); |
| |
| @Override |
| @Nullable |
| public ByteBuffer encodeMessage(@Nullable Object message) { |
| if (message == null) { |
| return null; |
| } |
| final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream(); |
| writeValue(stream, message); |
| final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size()); |
| buffer.put(stream.buffer(), 0, stream.size()); |
| return buffer; |
| } |
| |
| @Override |
| @Nullable |
| public Object decodeMessage(@Nullable ByteBuffer message) { |
| if (message == null) { |
| return null; |
| } |
| message.order(ByteOrder.nativeOrder()); |
| final Object value = readValue(message); |
| if (message.hasRemaining()) { |
| throw new IllegalArgumentException("Message corrupted"); |
| } |
| return value; |
| } |
| |
| private static final boolean LITTLE_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; |
| private static final Charset UTF8 = Charset.forName("UTF8"); |
| private static final byte NULL = 0; |
| private static final byte TRUE = 1; |
| private static final byte FALSE = 2; |
| private static final byte INT = 3; |
| private static final byte LONG = 4; |
| private static final byte BIGINT = 5; |
| private static final byte DOUBLE = 6; |
| private static final byte STRING = 7; |
| private static final byte BYTE_ARRAY = 8; |
| private static final byte INT_ARRAY = 9; |
| private static final byte LONG_ARRAY = 10; |
| private static final byte DOUBLE_ARRAY = 11; |
| private static final byte LIST = 12; |
| private static final byte MAP = 13; |
| private static final byte FLOAT_ARRAY = 14; |
| |
| /** |
| * Writes an int representing a size to the specified stream. Uses an expanding code of 1 to 5 |
| * bytes to optimize for small values. |
| */ |
| protected static final void writeSize(@NonNull ByteArrayOutputStream stream, int value) { |
| if (BuildConfig.DEBUG && 0 > value) { |
| Log.e(TAG, "Attempted to write a negative size."); |
| } |
| if (value < 254) { |
| stream.write(value); |
| } else if (value <= 0xffff) { |
| stream.write(254); |
| writeChar(stream, value); |
| } else { |
| stream.write(255); |
| writeInt(stream, value); |
| } |
| } |
| |
| /** Writes the least significant two bytes of the specified int to the specified stream. */ |
| protected static final void writeChar(@NonNull ByteArrayOutputStream stream, int value) { |
| if (LITTLE_ENDIAN) { |
| stream.write(value); |
| stream.write(value >>> 8); |
| } else { |
| stream.write(value >>> 8); |
| stream.write(value); |
| } |
| } |
| |
| /** Writes the specified int as 4 bytes to the specified stream. */ |
| protected static final void writeInt(@NonNull ByteArrayOutputStream stream, int value) { |
| if (LITTLE_ENDIAN) { |
| stream.write(value); |
| stream.write(value >>> 8); |
| stream.write(value >>> 16); |
| stream.write(value >>> 24); |
| } else { |
| stream.write(value >>> 24); |
| stream.write(value >>> 16); |
| stream.write(value >>> 8); |
| stream.write(value); |
| } |
| } |
| |
| /** Writes the specified long as 8 bytes to the specified stream. */ |
| protected static final void writeLong(@NonNull ByteArrayOutputStream stream, long value) { |
| if (LITTLE_ENDIAN) { |
| stream.write((byte) value); |
| stream.write((byte) (value >>> 8)); |
| stream.write((byte) (value >>> 16)); |
| stream.write((byte) (value >>> 24)); |
| stream.write((byte) (value >>> 32)); |
| stream.write((byte) (value >>> 40)); |
| stream.write((byte) (value >>> 48)); |
| stream.write((byte) (value >>> 56)); |
| } else { |
| stream.write((byte) (value >>> 56)); |
| stream.write((byte) (value >>> 48)); |
| stream.write((byte) (value >>> 40)); |
| stream.write((byte) (value >>> 32)); |
| stream.write((byte) (value >>> 24)); |
| stream.write((byte) (value >>> 16)); |
| stream.write((byte) (value >>> 8)); |
| stream.write((byte) value); |
| } |
| } |
| |
| /** Writes the specified double as 4 bytes to the specified stream */ |
| protected static final void writeFloat(@NonNull ByteArrayOutputStream stream, float value) { |
| writeInt(stream, Float.floatToIntBits(value)); |
| } |
| |
| /** Writes the specified double as 8 bytes to the specified stream. */ |
| protected static final void writeDouble(@NonNull ByteArrayOutputStream stream, double value) { |
| writeLong(stream, Double.doubleToLongBits(value)); |
| } |
| |
| /** Writes the length and then the actual bytes of the specified array to the specified stream. */ |
| protected static final void writeBytes( |
| @NonNull ByteArrayOutputStream stream, @NonNull byte[] bytes) { |
| writeSize(stream, bytes.length); |
| stream.write(bytes, 0, bytes.length); |
| } |
| |
| /** |
| * Writes a number of padding bytes to the specified stream to ensure that the next value is |
| * aligned to a whole multiple of the specified alignment. An example usage with alignment = 8 is |
| * to ensure doubles are word-aligned in the stream. |
| */ |
| protected static final void writeAlignment(@NonNull ByteArrayOutputStream stream, int alignment) { |
| final int mod = stream.size() % alignment; |
| if (mod != 0) { |
| for (int i = 0; i < alignment - mod; i++) { |
| stream.write(0); |
| } |
| } |
| } |
| |
| /** |
| * Writes a type discriminator byte and then a byte serialization of the specified value to the |
| * specified stream. |
| * |
| * <p>Subclasses can extend the codec by overriding this method, calling super for values that the |
| * extension does not handle. |
| */ |
| protected void writeValue(@NonNull ByteArrayOutputStream stream, @Nullable Object value) { |
| if (value == null || value.equals(null)) { |
| stream.write(NULL); |
| } else if (value instanceof Boolean) { |
| stream.write(((Boolean) value).booleanValue() ? TRUE : FALSE); |
| } else if (value instanceof Number) { |
| if (value instanceof Integer || value instanceof Short || value instanceof Byte) { |
| stream.write(INT); |
| writeInt(stream, ((Number) value).intValue()); |
| } else if (value instanceof Long) { |
| stream.write(LONG); |
| writeLong(stream, (long) value); |
| } else if (value instanceof Float || value instanceof Double) { |
| stream.write(DOUBLE); |
| writeAlignment(stream, 8); |
| writeDouble(stream, ((Number) value).doubleValue()); |
| } else if (value instanceof BigInteger) { |
| stream.write(BIGINT); |
| writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8)); |
| } else { |
| throw new IllegalArgumentException("Unsupported Number type: " + value.getClass()); |
| } |
| } else if (value instanceof CharSequence) { |
| stream.write(STRING); |
| writeBytes(stream, value.toString().getBytes(UTF8)); |
| } else if (value instanceof byte[]) { |
| stream.write(BYTE_ARRAY); |
| writeBytes(stream, (byte[]) value); |
| } else if (value instanceof int[]) { |
| stream.write(INT_ARRAY); |
| final int[] array = (int[]) value; |
| writeSize(stream, array.length); |
| writeAlignment(stream, 4); |
| for (final int n : array) { |
| writeInt(stream, n); |
| } |
| } else if (value instanceof long[]) { |
| stream.write(LONG_ARRAY); |
| final long[] array = (long[]) value; |
| writeSize(stream, array.length); |
| writeAlignment(stream, 8); |
| for (final long n : array) { |
| writeLong(stream, n); |
| } |
| } else if (value instanceof double[]) { |
| stream.write(DOUBLE_ARRAY); |
| final double[] array = (double[]) value; |
| writeSize(stream, array.length); |
| writeAlignment(stream, 8); |
| for (final double d : array) { |
| writeDouble(stream, d); |
| } |
| } else if (value instanceof List) { |
| stream.write(LIST); |
| final List<?> list = (List) value; |
| writeSize(stream, list.size()); |
| for (final Object o : list) { |
| writeValue(stream, o); |
| } |
| } else if (value instanceof Map) { |
| stream.write(MAP); |
| final Map<?, ?> map = (Map) value; |
| writeSize(stream, map.size()); |
| for (final Entry<?, ?> entry : map.entrySet()) { |
| writeValue(stream, entry.getKey()); |
| writeValue(stream, entry.getValue()); |
| } |
| } else if (value instanceof float[]) { |
| stream.write(FLOAT_ARRAY); |
| final float[] array = (float[]) value; |
| writeSize(stream, array.length); |
| writeAlignment(stream, 4); |
| for (final float f : array) { |
| writeFloat(stream, f); |
| } |
| } else { |
| throw new IllegalArgumentException( |
| "Unsupported value: '" + value + "' of type '" + value.getClass() + "'"); |
| } |
| } |
| |
| /** Reads an int representing a size as written by writeSize. */ |
| protected static final int readSize(@NonNull ByteBuffer buffer) { |
| if (!buffer.hasRemaining()) { |
| throw new IllegalArgumentException("Message corrupted"); |
| } |
| final int value = buffer.get() & 0xff; |
| if (value < 254) { |
| return value; |
| } else if (value == 254) { |
| return buffer.getChar(); |
| } else { |
| return buffer.getInt(); |
| } |
| } |
| |
| /** Reads a byte array as written by writeBytes. */ |
| @NonNull |
| protected static final byte[] readBytes(@NonNull ByteBuffer buffer) { |
| final int length = readSize(buffer); |
| final byte[] bytes = new byte[length]; |
| buffer.get(bytes); |
| return bytes; |
| } |
| |
| /** Reads alignment padding bytes as written by writeAlignment. */ |
| protected static final void readAlignment(@NonNull ByteBuffer buffer, int alignment) { |
| final int mod = buffer.position() % alignment; |
| if (mod != 0) { |
| buffer.position(buffer.position() + alignment - mod); |
| } |
| } |
| |
| /** Reads a value as written by writeValue. */ |
| @NonNull |
| protected final Object readValue(@NonNull ByteBuffer buffer) { |
| if (!buffer.hasRemaining()) { |
| throw new IllegalArgumentException("Message corrupted"); |
| } |
| final byte type = buffer.get(); |
| return readValueOfType(type, buffer); |
| } |
| |
| /** |
| * Reads a value of the specified type. |
| * |
| * <p>Subclasses may extend the codec by overriding this method, calling super for types that the |
| * extension does not handle. |
| */ |
| @Nullable |
| protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { |
| final Object result; |
| switch (type) { |
| case NULL: |
| result = null; |
| break; |
| case TRUE: |
| result = true; |
| break; |
| case FALSE: |
| result = false; |
| break; |
| case INT: |
| result = buffer.getInt(); |
| break; |
| case LONG: |
| result = buffer.getLong(); |
| break; |
| case BIGINT: |
| { |
| final byte[] hex = readBytes(buffer); |
| result = new BigInteger(new String(hex, UTF8), 16); |
| break; |
| } |
| case DOUBLE: |
| readAlignment(buffer, 8); |
| result = buffer.getDouble(); |
| break; |
| case STRING: |
| { |
| final byte[] bytes = readBytes(buffer); |
| result = new String(bytes, UTF8); |
| break; |
| } |
| case BYTE_ARRAY: |
| { |
| result = readBytes(buffer); |
| break; |
| } |
| case INT_ARRAY: |
| { |
| final int length = readSize(buffer); |
| final int[] array = new int[length]; |
| readAlignment(buffer, 4); |
| buffer.asIntBuffer().get(array); |
| result = array; |
| buffer.position(buffer.position() + 4 * length); |
| break; |
| } |
| case LONG_ARRAY: |
| { |
| final int length = readSize(buffer); |
| final long[] array = new long[length]; |
| readAlignment(buffer, 8); |
| buffer.asLongBuffer().get(array); |
| result = array; |
| buffer.position(buffer.position() + 8 * length); |
| break; |
| } |
| case DOUBLE_ARRAY: |
| { |
| final int length = readSize(buffer); |
| final double[] array = new double[length]; |
| readAlignment(buffer, 8); |
| buffer.asDoubleBuffer().get(array); |
| result = array; |
| buffer.position(buffer.position() + 8 * length); |
| break; |
| } |
| case LIST: |
| { |
| final int size = readSize(buffer); |
| final List<Object> list = new ArrayList<>(size); |
| for (int i = 0; i < size; i++) { |
| list.add(readValue(buffer)); |
| } |
| result = list; |
| break; |
| } |
| case MAP: |
| { |
| final int size = readSize(buffer); |
| final Map<Object, Object> map = new HashMap<>(); |
| for (int i = 0; i < size; i++) { |
| map.put(readValue(buffer), readValue(buffer)); |
| } |
| result = map; |
| break; |
| } |
| case FLOAT_ARRAY: |
| { |
| final int length = readSize(buffer); |
| final float[] array = new float[length]; |
| readAlignment(buffer, 4); |
| buffer.asFloatBuffer().get(array); |
| result = array; |
| buffer.position(buffer.position() + 4 * length); |
| break; |
| } |
| default: |
| throw new IllegalArgumentException("Message corrupted"); |
| } |
| return result; |
| } |
| |
| static final class ExposedByteArrayOutputStream extends ByteArrayOutputStream { |
| byte[] buffer() { |
| return buf; |
| } |
| } |
| } |