Merge "Extend the INVOKE_VIEW_METHOD of the "Generic View Operation" (VUOP) DDM handler to 1) Return the invoked method's return value 2) In addition to the primitive values, support String and byte[]" into tm-qpr-dev am: da9ab025d9 am: c55420dc4f

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19898484

Change-Id: I9283e48a1edb0780c7b1a6d4e51eea39b4959d59
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java
index 6b0f78f..0f66fcb 100644
--- a/core/java/android/ddm/DdmHandleViewDebug.java
+++ b/core/java/android/ddm/DdmHandleViewDebug.java
@@ -16,12 +16,16 @@
 
 package android.ddm;
 
+import static com.android.internal.util.Preconditions.checkArgument;
+
 import android.util.Log;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewRootImpl;
 import android.view.WindowManagerGlobal;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import org.apache.harmony.dalvik.ddmc.Chunk;
 import org.apache.harmony.dalvik.ddmc.ChunkHandler;
 import org.apache.harmony.dalvik.ddmc.DdmServer;
@@ -34,6 +38,7 @@
 import java.lang.reflect.Method;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 
 /**
  * Handle various requests related to profiling / debugging of the view system.
@@ -123,14 +128,15 @@
         }
 
         if (type == CHUNK_VURT) {
-            if (op == VURT_DUMP_HIERARCHY)
+            if (op == VURT_DUMP_HIERARCHY) {
                 return dumpHierarchy(rootView, in);
-            else if (op == VURT_CAPTURE_LAYERS)
+            } else if (op == VURT_CAPTURE_LAYERS) {
                 return captureLayers(rootView);
-            else if (op == VURT_DUMP_THEME)
+            } else if (op == VURT_DUMP_THEME) {
                 return dumpTheme(rootView);
-            else
+            } else {
                 return createFailChunk(ERR_INVALID_OP, "Unknown view root operation: " + op);
+            }
         }
 
         final View targetView = getTargetView(rootView, in);
@@ -207,9 +213,9 @@
     /**
      * Returns the view hierarchy and/or view properties starting at the provided view.
      * Based on the input options, the return data may include:
-     *  - just the view hierarchy
-     *  - view hierarchy & the properties for each of the views
-     *  - just the view properties for a specific view.
+     * - just the view hierarchy
+     * - view hierarchy & the properties for each of the views
+     * - just the view properties for a specific view.
      *  TODO: Currently this only returns views starting at the root, need to fix so that
      *  it can return properties of any view.
      */
@@ -220,7 +226,7 @@
 
         long start = System.currentTimeMillis();
 
-        ByteArrayOutputStream b = new ByteArrayOutputStream(2*1024*1024);
+        ByteArrayOutputStream b = new ByteArrayOutputStream(2 * 1024 * 1024);
         try {
             if (v2) {
                 ViewDebug.dumpv2(rootView, b);
@@ -304,17 +310,47 @@
      * Invokes provided method on the view.
      * The method name and its arguments are passed in as inputs via the byte buffer.
      * The buffer contains:<ol>
-     *  <li> len(method name) </li>
-     *  <li> method name </li>
-     *  <li> # of args </li>
-     *  <li> arguments: Each argument comprises of a type specifier followed by the actual argument.
-     *          The type specifier is a single character as used in JNI:
-     *          (Z - boolean, B - byte, C - char, S - short, I - int, J - long,
-     *          F - float, D - double). <p>
-     *          The type specifier is followed by the actual value of argument.
-     *          Booleans are encoded via bytes with 0 indicating false.</li>
+     * <li> len(method name) </li>
+     * <li> method name (encoded as UTF-16 2-byte characters) </li>
+     * <li> # of args </li>
+     * <li> arguments: Each argument comprises of a type specifier followed by the actual argument.
+     * The type specifier is one character modelled after JNI signatures:
+     *          <ul>
+     *              <li>[ - array<br>
+     *                This is followed by a second character according to this spec, indicating the
+     *                array type, then the array length as an Int, followed by a repeated encoding
+     *                of the actual data.
+     *                WARNING: Only <b>byte[]</b> is supported currently.
+     *              </li>
+     *              <li>Z - boolean<br>
+     *                 Booleans are encoded via bytes with 0 indicating false</li>
+     *              <li>B - byte</li>
+     *              <li>C - char</li>
+     *              <li>S - short</li>
+     *              <li>I - int</li>
+     *              <li>J - long</li>
+     *              <li>F - float</li>
+     *              <li>D - double</li>
+     *              <li>V - void<br>
+     *                NOT followed by a value. Only used for return types</li>
+     *              <li>R - String (not a real JNI type, but added for convenience)<br>
+     *                Strings are encoded as an unsigned short of the number of <b>bytes</b>,
+     *                followed by the actual UTF-8 encoded bytes.
+     *                WARNING: This is the same encoding as produced by
+     *                ViewHierarchyEncoder#writeString. However, note that this encoding is
+     *                different to what DdmHandle#getString() expects, which is used in other places
+     *                in this class.
+     *                WARNING: Since the length is the number of UTF-8 encoded bytes, Strings can
+     *                contain up to 64k ASCII characters, yet depending on the actual data, the true
+     *                maximum might be as little as 21844 unicode characters.
+     *                <b>null</b> String objects are encoded as an empty string
+     *              </li>
+     *            </ul>
+     *   </li>
      * </ol>
      * Methods that take no arguments need only specify the method name.
+     *
+     * The return value is encoded the same way as a single parameter (type + value)
      */
     private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) {
         int l = in.getInt();
@@ -327,54 +363,17 @@
             args = new Object[0];
         } else {
             int nArgs = in.getInt();
-
             argTypes = new Class<?>[nArgs];
             args = new Object[nArgs];
 
-            for (int i = 0; i < nArgs; i++) {
-                char c = in.getChar();
-                switch (c) {
-                    case 'Z':
-                        argTypes[i] = boolean.class;
-                        args[i] = in.get() == 0 ? false : true;
-                        break;
-                    case 'B':
-                        argTypes[i] = byte.class;
-                        args[i] = in.get();
-                        break;
-                    case 'C':
-                        argTypes[i] = char.class;
-                        args[i] = in.getChar();
-                        break;
-                    case 'S':
-                        argTypes[i] = short.class;
-                        args[i] = in.getShort();
-                        break;
-                    case 'I':
-                        argTypes[i] = int.class;
-                        args[i] = in.getInt();
-                        break;
-                    case 'J':
-                        argTypes[i] = long.class;
-                        args[i] = in.getLong();
-                        break;
-                    case 'F':
-                        argTypes[i] = float.class;
-                        args[i] = in.getFloat();
-                        break;
-                    case 'D':
-                        argTypes[i] = double.class;
-                        args[i] = in.getDouble();
-                        break;
-                    default:
-                        Log.e(TAG, "arg " + i + ", unrecognized type: " + c);
-                        return createFailChunk(ERR_INVALID_PARAM,
-                                "Unsupported parameter type (" + c + ") to invoke view method.");
-                }
+            try {
+                deserializeMethodParameters(args, argTypes, in);
+            } catch (ViewMethodInvocationSerializationException e) {
+                return createFailChunk(ERR_INVALID_PARAM, e.getMessage());
             }
         }
 
-        Method method = null;
+        Method method;
         try {
             method = targetView.getClass().getMethod(methodName, argTypes);
         } catch (NoSuchMethodException e) {
@@ -384,7 +383,10 @@
         }
 
         try {
-            ViewDebug.invokeViewMethod(targetView, method, args);
+            Object result = ViewDebug.invokeViewMethod(targetView, method, args);
+            Class<?> returnType = method.getReturnType();
+            byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result));
+            return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length);
         } catch (Exception e) {
             Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
             String msg = e.getCause().getMessage();
@@ -393,8 +395,6 @@
             }
             return createFailChunk(ERR_EXCEPTION, msg);
         }
-
-        return null;
     }
 
     private Chunk setLayoutParameter(final View rootView, final View targetView, ByteBuffer in) {
@@ -406,7 +406,7 @@
         } catch (Exception e) {
             Log.e(TAG, "Exception setting layout parameter: " + e);
             return createFailChunk(ERR_EXCEPTION, "Error accessing field "
-                        + param + ":" + e.getMessage());
+                    + param + ":" + e.getMessage());
         }
 
         return null;
@@ -431,4 +431,175 @@
         byte[] data = b.toByteArray();
         return new Chunk(CHUNK_VUOP, data, 0, data.length);
     }
+
+    /**
+     * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
+     * buffer.
+     *
+     * The length of {@code args} determines how many arguments are read. The {@code argTypes} must
+     * be the same length, and will be set to the argument types of the data read.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static void deserializeMethodParameters(
+            Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
+            ViewMethodInvocationSerializationException {
+        checkArgument(args.length == argTypes.length);
+
+        for (int i = 0; i < args.length; i++) {
+            char typeSignature = in.getChar();
+            boolean isArray = typeSignature == SIG_ARRAY;
+            if (isArray) {
+                char arrayType = in.getChar();
+                if (arrayType != SIG_BYTE) {
+                    // This implementation only supports byte-arrays for now.
+                    throw new ViewMethodInvocationSerializationException(
+                            "Unsupported array parameter type (" + typeSignature
+                                    + ") to invoke view method @argument " + i);
+                }
+
+                int arrayLength = in.getInt();
+                if (arrayLength > in.remaining()) {
+                    // The sender did not actually sent the specified amount of bytes. This
+                    // avoids a malformed packet to trigger an out-of-memory error.
+                    throw new BufferUnderflowException();
+                }
+
+                byte[] byteArray = new byte[arrayLength];
+                in.get(byteArray);
+
+                argTypes[i] = byte[].class;
+                args[i] = byteArray;
+            } else {
+                switch (typeSignature) {
+                    case SIG_BOOLEAN:
+                        argTypes[i] = boolean.class;
+                        args[i] = in.get() != 0;
+                        break;
+                    case SIG_BYTE:
+                        argTypes[i] = byte.class;
+                        args[i] = in.get();
+                        break;
+                    case SIG_CHAR:
+                        argTypes[i] = char.class;
+                        args[i] = in.getChar();
+                        break;
+                    case SIG_SHORT:
+                        argTypes[i] = short.class;
+                        args[i] = in.getShort();
+                        break;
+                    case SIG_INT:
+                        argTypes[i] = int.class;
+                        args[i] = in.getInt();
+                        break;
+                    case SIG_LONG:
+                        argTypes[i] = long.class;
+                        args[i] = in.getLong();
+                        break;
+                    case SIG_FLOAT:
+                        argTypes[i] = float.class;
+                        args[i] = in.getFloat();
+                        break;
+                    case SIG_DOUBLE:
+                        argTypes[i] = double.class;
+                        args[i] = in.getDouble();
+                        break;
+                    case SIG_STRING: {
+                        argTypes[i] = String.class;
+                        int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
+                        byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
+                        in.get(rawStringBuffer);
+                        args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
+                        break;
+                    }
+                    default:
+                        Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
+                        throw new ViewMethodInvocationSerializationException(
+                                "Unsupported parameter type (" + typeSignature
+                                        + ") to invoke view method.");
+                }
+            }
+
+        }
+    }
+
+    /**
+     * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
+     * @hide
+     */
+    @VisibleForTesting
+    public static byte[] serializeReturnValue(Class<?> type, Object value)
+            throws ViewMethodInvocationSerializationException, IOException {
+        ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
+        DataOutputStream dos = new DataOutputStream(byteOutStream);
+
+        if (type.isArray()) {
+            if (!type.equals(byte[].class)) {
+                // Only byte arrays are supported currently.
+                throw new ViewMethodInvocationSerializationException(
+                        "Unsupported array return type (" + type + ")");
+            }
+            byte[] byteArray = (byte[]) value;
+            dos.writeChar(SIG_ARRAY);
+            dos.writeChar(SIG_BYTE);
+            dos.writeInt(byteArray.length);
+            dos.write(byteArray);
+        } else if (boolean.class.equals(type)) {
+            dos.writeChar(SIG_BOOLEAN);
+            dos.write((boolean) value ? 1 : 0);
+        } else if (byte.class.equals(type)) {
+            dos.writeChar(SIG_BYTE);
+            dos.writeByte((byte) value);
+        } else if (char.class.equals(type)) {
+            dos.writeChar(SIG_CHAR);
+            dos.writeChar((char) value);
+        } else if (short.class.equals(type)) {
+            dos.writeChar(SIG_SHORT);
+            dos.writeShort((short) value);
+        } else if (int.class.equals(type)) {
+            dos.writeChar(SIG_INT);
+            dos.writeInt((int) value);
+        } else if (long.class.equals(type)) {
+            dos.writeChar(SIG_LONG);
+            dos.writeLong((long) value);
+        } else if (double.class.equals(type)) {
+            dos.writeChar(SIG_DOUBLE);
+            dos.writeDouble((double) value);
+        } else if (float.class.equals(type)) {
+            dos.writeChar(SIG_FLOAT);
+            dos.writeFloat((float) value);
+        } else if (String.class.equals(type)) {
+            dos.writeChar(SIG_STRING);
+            dos.writeUTF(value != null ? (String) value : "");
+        } else {
+            dos.writeChar(SIG_VOID);
+        }
+
+        return byteOutStream.toByteArray();
+    }
+
+    // Prefixes for simple primitives. These match the JNI definitions.
+    private static final char SIG_ARRAY = '[';
+    private static final char SIG_BOOLEAN = 'Z';
+    private static final char SIG_BYTE = 'B';
+    private static final char SIG_SHORT = 'S';
+    private static final char SIG_CHAR = 'C';
+    private static final char SIG_INT = 'I';
+    private static final char SIG_LONG = 'J';
+    private static final char SIG_FLOAT = 'F';
+    private static final char SIG_DOUBLE = 'D';
+    private static final char SIG_VOID = 'V';
+    // Prefixes for some commonly used objects
+    private static final char SIG_STRING = 'R';
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static class ViewMethodInvocationSerializationException extends Exception {
+        ViewMethodInvocationSerializationException(String message) {
+            super(message);
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java b/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java
new file mode 100644
index 0000000..7248983
--- /dev/null
+++ b/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters;
+import static android.ddm.DdmHandleViewDebug.serializeReturnValue;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public final class DdmHandleViewDebugTest {
+    // true
+    private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1};
+
+    @Test
+    public void serializeReturnValue_booleanTrue() throws Exception {
+        assertArrayEquals(SERIALIZED_BOOLEAN_TRUE, serializeReturnValue(boolean.class, true));
+    }
+
+    @Test
+    public void deserializeMethodParameters_booleanTrue() throws Exception {
+        expectDeserializedArgument(boolean.class, true, SERIALIZED_BOOLEAN_TRUE);
+    }
+
+    // false
+    private static final byte[] SERIALIZED_BOOLEAN_FALSE = {0x00, 0x5A, 0};
+
+    @Test
+    public void serializeReturnValue_booleanFalse() throws Exception {
+        assertArrayEquals(SERIALIZED_BOOLEAN_FALSE, serializeReturnValue(boolean.class, false));
+    }
+
+    @Test
+    public void deserializeMethodParameters_booleanFalse() throws Exception {
+        expectDeserializedArgument(boolean.class, false, SERIALIZED_BOOLEAN_FALSE);
+    }
+
+    // (byte) 42
+    private static final byte[] SERIALIZED_BYTE = {0x00, 0x42, 42};
+
+    @Test
+    public void serializeReturnValue_byte() throws Exception {
+        assertArrayEquals(SERIALIZED_BYTE, serializeReturnValue(byte.class, (byte) 42));
+    }
+
+    @Test
+    public void deserializeMethodParameters_byte() throws Exception {
+        expectDeserializedArgument(byte.class, (byte) 42, SERIALIZED_BYTE);
+    }
+
+    // '\u1122'
+    private static final byte[] SERIALIZED_CHAR = {0x00, 0x43, 0x11, 0x22};
+
+    @Test
+    public void serializeReturnValue_char() throws Exception {
+        assertArrayEquals(SERIALIZED_CHAR, serializeReturnValue(char.class, '\u1122'));
+    }
+
+    @Test
+    public void deserializeMethodParameters_char() throws Exception {
+        expectDeserializedArgument(char.class, '\u1122', SERIALIZED_CHAR);
+    }
+
+    // (short) 0x1011
+    private static final byte[] SERIALIZED_SHORT = {0x00, 0x53, 0x10, 0x11};
+
+    @Test
+    public void serializeReturnValue_short() throws Exception {
+        assertArrayEquals(SERIALIZED_SHORT,
+                serializeReturnValue(short.class, (short) 0x1011));
+    }
+
+    @Test
+    public void deserializeMethodParameters_short() throws Exception {
+        expectDeserializedArgument(short.class, (short) 0x1011, SERIALIZED_SHORT);
+    }
+
+    // 0x11223344
+    private static final byte[] SERIALIZED_INT = {0x00, 0x49, 0x11, 0x22, 0x33, 0x44};
+
+    @Test
+    public void serializeReturnValue_int() throws Exception {
+        assertArrayEquals(SERIALIZED_INT,
+                serializeReturnValue(int.class, 0x11223344));
+    }
+
+    @Test
+    public void deserializeMethodParameters_int() throws Exception {
+        expectDeserializedArgument(int.class, 0x11223344, SERIALIZED_INT);
+    }
+
+    // 0x0011223344556677L
+    private static final byte[] SERIALIZED_LONG =
+            {0x00, 0x4a, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
+
+    @Test
+    public void serializeReturnValue_long() throws Exception {
+        assertArrayEquals(SERIALIZED_LONG,
+                serializeReturnValue(long.class, 0x0011223344556677L));
+    }
+
+    @Test
+    public void deserializeMethodParameters_long() throws Exception {
+        expectDeserializedArgument(long.class, 0x0011223344556677L, SERIALIZED_LONG);
+    }
+
+    // 3.141d
+    private static final byte[] SERIALIZED_DOUBLE =
+            {0x00, 0x44, (byte) 0x40, (byte) 0x09, (byte) 0x20, (byte) 0xc4, (byte) 0x9b,
+                    (byte) 0xa5, (byte) 0xe3, (byte) 0x54};
+
+    @Test
+    public void serializeReturnValue_double() throws Exception {
+        assertArrayEquals(
+                SERIALIZED_DOUBLE,
+                serializeReturnValue(double.class, 3.141d));
+    }
+
+    @Test
+    public void deserializeMethodParameters_double() throws Exception {
+        expectDeserializedArgument(double.class, 3.141d, SERIALIZED_DOUBLE);
+    }
+
+    // 3.141f
+    private static final byte[] SERIALIZED_FLOAT =
+            {0x00, 0x46, (byte) 0x40, (byte) 0x49, (byte) 0x06, (byte) 0x25};
+
+    @Test
+    public void serializeReturnValue_float() throws Exception {
+        assertArrayEquals(SERIALIZED_FLOAT,
+                serializeReturnValue(float.class, 3.141f));
+    }
+
+    @Test
+    public void deserializeMethodParameters_float() throws Exception {
+        expectDeserializedArgument(float.class, 3.141f, SERIALIZED_FLOAT);
+    }
+
+    // "foo"
+    private static final byte[] SERIALIZED_ASCII_STRING = {0x00, 0x52, 0, 3, 0x66, 0x6f, 0x6f};
+
+    @Test
+    public void serializeReturnValue_asciiString() throws Exception {
+        assertArrayEquals(SERIALIZED_ASCII_STRING,
+                serializeReturnValue(String.class, "foo"));
+    }
+
+    @Test
+    public void deserializeMethodParameters_asciiString() throws Exception {
+        expectDeserializedArgument(String.class, "foo", SERIALIZED_ASCII_STRING);
+    }
+
+    // "\u1122"
+    private static final byte[] SERIALIZED_NON_ASCII_STRING =
+            {0x00, 0x52, 0, 3, (byte) 0xe1, (byte) 0x84, (byte) 0xa2};
+
+    @Test
+    public void serializeReturnValue_nonAsciiString_encodesAsUtf8() throws Exception {
+        assertArrayEquals(SERIALIZED_NON_ASCII_STRING,
+                serializeReturnValue(String.class, "\u1122"));
+    }
+
+    @Test
+    public void deserializeMethodParameters_decodesFromUtf8() throws Exception {
+        expectDeserializedArgument(String.class, "\u1122", SERIALIZED_NON_ASCII_STRING);
+    }
+
+    // ""
+    private static final byte[] SERIALIZED_EMPTY_STRING = {0x00, 0x52, 0, 0};
+
+    @Test
+    public void serializeReturnValue_emptyString() throws Exception {
+        assertArrayEquals(SERIALIZED_EMPTY_STRING, serializeReturnValue(String.class, ""));
+    }
+
+    @Test
+    public void deserializeMethodParameters_emptyString() throws Exception {
+        expectDeserializedArgument(String.class, "", SERIALIZED_EMPTY_STRING);
+    }
+
+    @Test
+    public void serializeReturnValue_nullString_encodesAsEmptyString() throws Exception {
+        assertArrayEquals(new byte[]{0x00, 0x52, 0, 0}, serializeReturnValue(String.class, null));
+    }
+
+    // Illegal - string length exceeding actual bytes
+    private static final byte[] SERIALIZED_INVALID_STRING =
+            {0x00, 0x52, 0, 3, 0x66};
+
+    @Test
+    public void deserializeMethodParameters_stringPayloadMissing_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(BufferUnderflowException.class,
+                () -> deserializeMethodParameters(args, argTypes,
+                        ByteBuffer.wrap(SERIALIZED_INVALID_STRING)));
+    }
+
+    @Test
+    public void serializeAndDeserialize_handlesStringsUpTo64k() throws Exception {
+        char[] chars = new char[65535];
+        Arrays.fill(chars, 'a');
+        String original = new String(chars);
+        byte[] serialized = serializeReturnValue(String.class, original);
+
+        // 2 bytes for the R signature char, 2 bytes char string byte count, 2^16-1 bytes ASCII
+        // payload
+        assertEquals(2 + 2 + 65535, serialized.length);
+
+        // length is unsigned short
+        assertArrayEquals(new byte[]{0x00, 0x52, (byte) 0xff, (byte) 0xff},
+                Arrays.copyOfRange(serialized, 0, 4));
+
+        // length of string must be interpreted as unsigned short, returning original content
+        expectDeserializedArgument(String.class, original, serialized);
+    }
+
+    private static final byte[] SERIALIZED_VOID = {0x00, 0x56};
+
+    @Test
+    public void serializeReturnValue_void() throws Exception {
+        assertArrayEquals(SERIALIZED_VOID, serializeReturnValue(void.class, null));
+    }
+
+    @Test
+    public void deserializeMethodParameters_void_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(ViewMethodInvocationSerializationException.class,
+                () -> deserializeMethodParameters(args, argTypes,
+                        ByteBuffer.wrap(SERIALIZED_VOID)));
+    }
+
+    // new byte[]{}
+    private static final byte[] SERIALIZED_EMPTY_BYTE_ARRAY = {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 0};
+
+    @Test
+    public void serializeReturnValue_emptyByteArray() throws Exception {
+        assertArrayEquals(SERIALIZED_EMPTY_BYTE_ARRAY,
+                serializeReturnValue(byte[].class, new byte[]{}));
+    }
+
+    @Test
+    public void deserializeMethodParameters_emptyByteArray() throws Exception {
+        expectDeserializedArgument(byte[].class, new byte[]{}, SERIALIZED_EMPTY_BYTE_ARRAY);
+    }
+
+    // new byte[]{0, 42}
+    private static final byte[] SERIALIZED_SIMPLE_BYTE_ARRAY =
+            {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 2, 0, 42};
+
+    @Test
+    public void serializeReturnValue_byteArray() throws Exception {
+        assertArrayEquals(SERIALIZED_SIMPLE_BYTE_ARRAY,
+                serializeReturnValue(byte[].class, new byte[]{0, 42}));
+    }
+
+    @Test
+    public void deserializeMethodParameters_byteArray() throws Exception {
+        expectDeserializedArgument(byte[].class, new byte[]{0, 42}, SERIALIZED_SIMPLE_BYTE_ARRAY);
+    }
+
+    @Test
+    public void serializeReturnValue_largeByteArray_encodesSizeCorrectly() throws Exception {
+        byte[] result = serializeReturnValue(byte[].class, new byte[0x012233]);
+        // 2 bytes for the each [Z signature char, 4 bytes int array length, 0x012233 bytes payload
+        assertEquals(2 + 2 + 4 + 74291, result.length);
+
+        assertArrayEquals(new byte[]{0x00, 0x5b, 0x00, 0x42, 0x00, 0x01, 0x22, 0x33},
+                Arrays.copyOfRange(result, 0, 8));
+    }
+
+    // Illegal - declared size exceeds remaining buffer length
+    private static final byte[] SERIALIZED_INVALID_BYTE_ARRAY =
+            {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 3, 0, 42};
+
+    @Test
+    public void deserializeMethodParameters_sizeExceedsBuffer_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(BufferUnderflowException.class,
+                () -> deserializeMethodParameters(args, argTypes,
+                        ByteBuffer.wrap(SERIALIZED_INVALID_BYTE_ARRAY)));
+    }
+
+    // new int[]{}
+    private static final byte[] SERIALIZED_EMPTY_INT_ARRAY = {0x00, 0x5b, 0x00, 0x49, 0, 0, 0, 0};
+
+    @Test
+    public void serializeReturnValue_nonByteArrayType_throws() throws Exception {
+        assertThrows(ViewMethodInvocationSerializationException.class,
+                () -> serializeReturnValue(int[].class, 42));
+    }
+
+    @Test
+    public void deserializeMethodParameters_nonByteArrayType_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(ViewMethodInvocationSerializationException.class,
+                () -> deserializeMethodParameters(args, argTypes,
+                        ByteBuffer.wrap(SERIALIZED_EMPTY_INT_ARRAY)));
+    }
+
+    // new byte[]{0, 42}
+    private static final byte[] SERIALIZED_MULTIPLE_PARAMETERS =
+            {0x00, 0x42, 42, 0x00, 0x5A, 1};
+
+    @Test
+    public void deserializeMethodParameters_multipleParameters() throws Exception {
+        expectDeserializedArguments(new Class[]{byte.class, boolean.class},
+                new Object[]{(byte) 42, true}, SERIALIZED_MULTIPLE_PARAMETERS);
+    }
+
+    // Illegal - type 'X'
+    private static final byte[] SERIALIZED_INVALID_UNKNOWN_TYPE = {0x00, 0x58};
+
+    @Test
+    public void deserializeMethodParameters_unknownType_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(ViewMethodInvocationSerializationException.class,
+                () -> deserializeMethodParameters(args, argTypes,
+                        ByteBuffer.wrap(SERIALIZED_INVALID_UNKNOWN_TYPE)));
+    }
+
+    @Test
+    public void deserializeMethodParameters_noArgumentsEmptyPacket_isNoop() throws Exception {
+        Object[] args = new Object[0];
+        Class<?>[] argTypes = new Class<?>[0];
+        deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0]));
+    }
+
+    @Test
+    public void deserializeMethodParameters_withArgumentsEmptyPacket_throws() throws Exception {
+        Object[] args = new Object[1];
+        Class<?>[] argTypes = new Class<?>[1];
+        assertThrows(BufferUnderflowException.class,
+                () -> deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0])));
+    }
+
+    private static void expectDeserializedArgument(Class<?> expectedType, Object expectedValue,
+            byte[] argumentBuffer) throws Exception {
+        expectDeserializedArguments(new Class[]{expectedType}, new Object[]{expectedValue},
+                argumentBuffer);
+    }
+
+    private static void expectDeserializedArguments(Class<?>[] expectedTypes,
+            Object[] expectedValues, byte[] argumentBuffer) throws Exception {
+        final int argCount = expectedTypes.length;
+        assertEquals("test helper not used correctly", argCount, expectedValues.length);
+        Object[] actualArgs = new Object[argCount];
+        Class<?>[] actualArgTypes = new Class<?>[argCount];
+
+        ByteBuffer buffer = ByteBuffer.wrap(argumentBuffer);
+        deserializeMethodParameters(actualArgs, actualArgTypes, buffer);
+
+        for (int i = 0; i < argCount; i++) {
+            String context = "argument " + i;
+            assertEquals(context, expectedTypes[i], actualArgTypes[i]);
+            if (byte[].class.equals(expectedTypes[i])) {
+                assertArrayEquals((byte[]) expectedValues[i], (byte[]) actualArgs[i]);
+            } else {
+                assertEquals(expectedValues[i], actualArgs[i]);
+            }
+        }
+    }
+}