diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java index af56137c8f3..fa21c71e950 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java @@ -12,8 +12,7 @@ import io.flutter.Log; import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; import io.flutter.plugin.common.BinaryMessenger.BinaryReply; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Locale; +import java.util.Arrays; /** * A named channel for communicating with the Flutter application using basic, asynchronous message @@ -143,13 +142,56 @@ public final class BasicMessageChannel { resizeChannelBuffer(messenger, name, newSize); } - static void resizeChannelBuffer( - @NonNull BinaryMessenger messenger, @NonNull String channel, int newSize) { - Charset charset = Charset.forName("UTF-8"); - String messageString = String.format(Locale.US, "resize\r%s\r%d", channel, newSize); - final byte[] bytes = messageString.getBytes(charset); + /** + * Toggles whether the channel should show warning messages when discarding messages due to + * overflow. When 'allowed' is true the channel is expected to overflow and warning messages will + * not be shown. + */ + public void allowChannelBufferOverflow(boolean allowed) { + allowChannelBufferOverflow(messenger, name, allowed); + } + + private static ByteBuffer packetFromEncodedMessage(ByteBuffer message) { + // Create a bytes array using the buffer content (messages.array() can not be used here). + message.flip(); + final byte[] bytes = new byte[message.remaining()]; + message.get(bytes); + + // The current Android Java/JNI platform message implementation assumes + // that all buffers passed to native are direct buffers. ByteBuffer packet = ByteBuffer.allocateDirect(bytes.length); packet.put(bytes); + + return packet; + } + + /** + * Adjusts the number of messages that will get buffered when sending messages to channels that + * aren't fully set up yet. For example, the engine isn't running yet or the channel's message + * handler isn't set up on the Dart side yet. + */ + public static void resizeChannelBuffer( + @NonNull BinaryMessenger messenger, @NonNull String channel, int newSize) { + final StandardMethodCodec codec = StandardMethodCodec.INSTANCE; + Object[] arguments = {channel, newSize}; + MethodCall methodCall = new MethodCall("resize", Arrays.asList(arguments)); + ByteBuffer message = codec.encodeMethodCall(methodCall); + ByteBuffer packet = packetFromEncodedMessage(message); + messenger.send(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL, packet); + } + + /** + * Toggles whether the channel should show warning messages when discarding messages due to + * overflow. When 'allowed' is true the channel is expected to overflow and warning messages will + * not be shown. + */ + public static void allowChannelBufferOverflow( + @NonNull BinaryMessenger messenger, @NonNull String channel, boolean allowed) { + final StandardMethodCodec codec = StandardMethodCodec.INSTANCE; + Object[] arguments = {channel, allowed}; + MethodCall methodCall = new MethodCall("overflow", Arrays.asList(arguments)); + ByteBuffer message = codec.encodeMethodCall(methodCall); + ByteBuffer packet = packetFromEncodedMessage(message); messenger.send(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL, packet); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java index 2d646a25147..3aa2698ad46 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java @@ -157,6 +157,15 @@ public class MethodChannel { BasicMessageChannel.resizeChannelBuffer(messenger, name, newSize); } + /** + * Toggles whether the channel should show warning messages when discarding messages due to + * overflow. When 'allowed' is true the channel is expected to overflow and warning messages will + * not be shown. + */ + public void allowChannelBufferOverflow(boolean allowed) { + BasicMessageChannel.allowChannelBufferOverflow(messenger, name, allowed); + } + /** A handler of incoming method calls. */ public interface MethodCallHandler { /** diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/common/MethodChannelTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/common/MethodChannelTest.java index c4ab750c2e9..8f8836f6148 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/common/MethodChannelTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/common/MethodChannelTest.java @@ -1,6 +1,7 @@ package io.flutter.plugin.common; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -11,17 +12,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Locale; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; import org.robolectric.annotation.Config; @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class MethodChannelTest { @Test - public void methodChannel_resizeChannelBuffer() { + public void resizeChannelBufferMessageIsWellformed() { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class)); String channel = "flutter/test"; @@ -30,15 +30,68 @@ public class MethodChannelTest { int newSize = 3; rawChannel.resizeChannelBuffer(newSize); - Charset charset = Charset.forName("UTF-8"); - String messageString = String.format(Locale.US, "resize\r%s\r%d", channel, newSize); - final byte[] bytes = messageString.getBytes(charset); - ByteBuffer packet = ByteBuffer.allocateDirect(bytes.length); - packet.put(bytes); + // Created from the following Dart code: + // MethodCall methodCall = const MethodCall('resize', ['flutter/test', 3]); + // const StandardMethodCodec().encodeMethodCall(methodCall).buffer.asUint8List(); + final byte[] expected = { + 7, 6, 114, 101, 115, 105, 122, 101, 12, 2, 7, 12, 102, 108, 117, 116, 116, 101, 114, 47, 116, + 101, 115, 116, 3, 3, 0, 0, 0 + }; - // Verify that DartExecutor sent the correct message to FlutterJNI. + // Verify that the correct message was sent to FlutterJNI. + ArgumentMatcher packetMatcher = + new ByteBufferContentMatcher(ByteBuffer.wrap(expected)); verify(mockFlutterJNI, times(1)) .dispatchPlatformMessage( - eq(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL), eq(packet), anyInt(), anyInt()); + eq(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL), + argThat(packetMatcher), + anyInt(), + anyInt()); + } + + @Test + public void overflowChannelBufferMessageIsWellformed() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class)); + String channel = "flutter/test"; + MethodChannel rawChannel = new MethodChannel(dartExecutor, channel); + + rawChannel.allowChannelBufferOverflow(true); + + // Created from the following Dart code: + // MethodCall methodCall = const MethodCall('overflow', ['flutter/test', true]); + // const StandardMethodCodec().encodeMethodCall(methodCall).buffer.asUint8List(); + final byte[] expected = { + 7, 8, 111, 118, 101, 114, 102, 108, 111, 119, 12, 2, 7, 12, 102, 108, 117, 116, 116, 101, 114, + 47, 116, 101, 115, 116, 1 + }; + + // Verify that the correct message was sent to FlutterJNI. + ArgumentMatcher packetMatcher = + new ByteBufferContentMatcher(ByteBuffer.wrap(expected)); + verify(mockFlutterJNI, times(1)) + .dispatchPlatformMessage( + eq(BasicMessageChannel.CHANNEL_BUFFERS_CHANNEL), + argThat(packetMatcher), + anyInt(), + anyInt()); + } +} + +// Custom ByteBuffer matcher which calls rewind on both buffers before calling equals. +// ByteBuffer.equals might return true when comparing byte buffers with different content if +// both have no remaining elements. +class ByteBufferContentMatcher implements ArgumentMatcher { + private ByteBuffer expected; + + public ByteBufferContentMatcher(ByteBuffer expected) { + this.expected = expected; + } + + @Override + public boolean matches(ByteBuffer received) { + expected.rewind(); + received.rewind(); + return received.equals(expected); } }