diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index 11cf6c48225c6..05d4488d99e39 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -37,7 +37,151 @@ public class PlatformChannel { private PlatformMessageHandler platformMessageHandler; @NonNull @VisibleForTesting - protected final PlatformMethodCallHandler parsingMethodCallHandler = new PlatformMethodCallHandler(); + protected final MethodChannel.MethodCallHandler parsingMethodCallHandler = new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (platformMessageHandler == null) { + // If no explicit PlatformMessageHandler has been registered then we don't + // need to forward this call to an API. Return. + return; + } + + String method = call.method; + Object arguments = call.arguments; + Log.v(TAG, "Received '" + method + "' message."); + try { + switch (method) { + case "SystemSound.play": + try { + SoundType soundType = SoundType.fromValue((String) arguments); + platformMessageHandler.playSystemSound(soundType); + result.success(null); + } catch (NoSuchFieldException exception) { + // The desired sound type does not exist. + result.error("error", exception.getMessage(), null); + } + break; + case "HapticFeedback.vibrate": + try { + HapticFeedbackType feedbackType = HapticFeedbackType.fromValue((String) arguments); + platformMessageHandler.vibrateHapticFeedback(feedbackType); + result.success(null); + } catch (NoSuchFieldException exception) { + // The desired feedback type does not exist. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemChrome.setPreferredOrientations": + try { + int androidOrientation = decodeOrientations((JSONArray) arguments); + platformMessageHandler.setPreferredOrientations(androidOrientation); + result.success(null); + } catch (JSONException | NoSuchFieldException exception) { + // JSONException: One or more expected fields were either omitted or referenced an invalid type. + // NoSuchFieldException: One or more expected fields were either omitted or referenced an invalid type. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemChrome.setApplicationSwitcherDescription": + try { + AppSwitcherDescription description = decodeAppSwitcherDescription((JSONObject) arguments); + platformMessageHandler.setApplicationSwitcherDescription(description); + result.success(null); + } catch (JSONException exception) { + // One or more expected fields were either omitted or referenced an invalid type. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemChrome.setEnabledSystemUIOverlays": + try { + List overlays = decodeSystemUiOverlays((JSONArray) arguments); + platformMessageHandler.showSystemOverlays(overlays); + result.success(null); + } catch (JSONException | NoSuchFieldException exception) { + // JSONException: One or more expected fields were either omitted or referenced an invalid type. + // NoSuchFieldException: One or more of the overlay names are invalid. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemChrome.restoreSystemUIOverlays": + platformMessageHandler.restoreSystemUiOverlays(); + result.success(null); + break; + case "SystemChrome.setSystemUIOverlayStyle": + try { + SystemChromeStyle systemChromeStyle = decodeSystemChromeStyle((JSONObject) arguments); + platformMessageHandler.setSystemUiOverlayStyle(systemChromeStyle); + result.success(null); + } catch (JSONException | NoSuchFieldException exception) { + // JSONException: One or more expected fields were either omitted or referenced an invalid type. + // NoSuchFieldException: One or more of the brightness names are invalid. + result.error("error", exception.getMessage(), null); + } + break; + case "SystemNavigator.pop": + platformMessageHandler.popSystemNavigator(); + result.success(null); + break; + case "SystemGestures.getSystemGestureExclusionRects": + List exclusionRects = platformMessageHandler.getSystemGestureExclusionRects(); + if (exclusionRects == null) { + String incorrectApiLevel = "Exclusion rects only exist for Android API 29+."; + result.error("error", incorrectApiLevel, null); + break; + } + + ArrayList> encodedExclusionRects = encodeExclusionRects(exclusionRects); + result.success(encodedExclusionRects); + break; + case "SystemGestures.setSystemGestureExclusionRects": + if (!(arguments instanceof JSONArray)) { + String inputTypeError = "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; + result.error("inputTypeError", inputTypeError, null); + break; + } + + JSONArray inputRects = (JSONArray) arguments; + ArrayList decodedRects = decodeExclusionRects(inputRects); + platformMessageHandler.setSystemGestureExclusionRects(decodedRects); + result.success(null); + break; + case "Clipboard.getData": { + String contentFormatName = (String) arguments; + ClipboardContentFormat clipboardFormat = null; + if (contentFormatName != null) { + try { + clipboardFormat = ClipboardContentFormat.fromValue(contentFormatName); + } catch (NoSuchFieldException exception) { + // An unsupported content format was requested. Return failure. + result.error("error", "No such clipboard content format: " + contentFormatName, null); + } + } + + CharSequence clipboardContent = platformMessageHandler.getClipboardData(clipboardFormat); + if (clipboardContent != null) { + JSONObject response = new JSONObject(); + response.put("text", clipboardContent); + result.success(response); + } else { + result.success(null); + } + break; + } + case "Clipboard.setData": { + String clipboardContent = ((JSONObject) arguments).getString("text"); + platformMessageHandler.setClipboardData(clipboardContent); + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } catch (JSONException e) { + result.error("error", "JSON error: " + e.getMessage(), null); + } + } + }; /** * Constructs a {@code PlatformChannel} that connects Android to the Dart code @@ -143,6 +287,13 @@ private int decodeOrientations(@NonNull JSONArray encodedOrientations) throws JS /** * Decodes a JSONArray of rectangle data into an ArrayList. * + * Since View.setSystemGestureExclusionRects receives a JSONArray containing + * JSONObjects, these values need to be transformed into the expected input + * of View.setSystemGestureExclusionRects, which is ArrayList. + * + * This method is used by the SystemGestures.setSystemGestureExclusionRects + * platform channel. + * * @throws JSONException if {@code inputRects} does not contain expected keys and value types. */ @NonNull @@ -535,159 +686,6 @@ public SystemChromeStyle( } } - /** - * A handler of incoming platform channel method calls received from Flutter. - * It first determines the platform's API to be called. If it exists, it then - * decodes incoming arguments, if needed, into a format that is necessary for the API. - */ - @VisibleForTesting - protected class PlatformMethodCallHandler implements MethodChannel.MethodCallHandler { - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - if (platformMessageHandler == null) { - // If no explicit PlatformMessageHandler has been registered then we don't - // need to forward this call to an API. Return. - return; - } - - String method = call.method; - Object arguments = call.arguments; - Log.v(TAG, "Received '" + method + "' message."); - try { - switch (method) { - case "SystemSound.play": - try { - SoundType soundType = SoundType.fromValue((String) arguments); - platformMessageHandler.playSystemSound(soundType); - result.success(null); - } catch (NoSuchFieldException exception) { - // The desired sound type does not exist. - result.error("error", exception.getMessage(), null); - } - break; - case "HapticFeedback.vibrate": - try { - HapticFeedbackType feedbackType = HapticFeedbackType.fromValue((String) arguments); - platformMessageHandler.vibrateHapticFeedback(feedbackType); - result.success(null); - } catch (NoSuchFieldException exception) { - // The desired feedback type does not exist. - result.error("error", exception.getMessage(), null); - } - break; - case "SystemChrome.setPreferredOrientations": - try { - int androidOrientation = decodeOrientations((JSONArray) arguments); - platformMessageHandler.setPreferredOrientations(androidOrientation); - result.success(null); - } catch (JSONException | NoSuchFieldException exception) { - // JSONException: One or more expected fields were either omitted or referenced an invalid type. - // NoSuchFieldException: One or more expected fields were either omitted or referenced an invalid type. - result.error("error", exception.getMessage(), null); - } - break; - case "SystemChrome.setApplicationSwitcherDescription": - try { - AppSwitcherDescription description = decodeAppSwitcherDescription((JSONObject) arguments); - platformMessageHandler.setApplicationSwitcherDescription(description); - result.success(null); - } catch (JSONException exception) { - // One or more expected fields were either omitted or referenced an invalid type. - result.error("error", exception.getMessage(), null); - } - break; - case "SystemChrome.setEnabledSystemUIOverlays": - try { - List overlays = decodeSystemUiOverlays((JSONArray) arguments); - platformMessageHandler.showSystemOverlays(overlays); - result.success(null); - } catch (JSONException | NoSuchFieldException exception) { - // JSONException: One or more expected fields were either omitted or referenced an invalid type. - // NoSuchFieldException: One or more of the overlay names are invalid. - result.error("error", exception.getMessage(), null); - } - break; - case "SystemChrome.restoreSystemUIOverlays": - platformMessageHandler.restoreSystemUiOverlays(); - result.success(null); - break; - case "SystemChrome.setSystemUIOverlayStyle": - try { - SystemChromeStyle systemChromeStyle = decodeSystemChromeStyle((JSONObject) arguments); - platformMessageHandler.setSystemUiOverlayStyle(systemChromeStyle); - result.success(null); - } catch (JSONException | NoSuchFieldException exception) { - // JSONException: One or more expected fields were either omitted or referenced an invalid type. - // NoSuchFieldException: One or more of the brightness names are invalid. - result.error("error", exception.getMessage(), null); - } - break; - case "SystemNavigator.pop": - platformMessageHandler.popSystemNavigator(); - result.success(null); - break; - case "Clipboard.getData": { - String contentFormatName = (String) arguments; - ClipboardContentFormat clipboardFormat = null; - if (contentFormatName != null) { - try { - clipboardFormat = ClipboardContentFormat.fromValue(contentFormatName); - } catch (NoSuchFieldException exception) { - // An unsupported content format was requested. Return failure. - result.error("error", "No such clipboard content format: " + contentFormatName, null); - } - } - - CharSequence clipboardContent = platformMessageHandler.getClipboardData(clipboardFormat); - if (clipboardContent != null) { - JSONObject response = new JSONObject(); - response.put("text", clipboardContent); - result.success(response); - } else { - result.success(null); - } - break; - } - case "Clipboard.setData": { - String clipboardContent = ((JSONObject) arguments).getString("text"); - platformMessageHandler.setClipboardData(clipboardContent); - result.success(null); - break; - } - case "SystemGestures.setSystemGestureExclusionRects": - if (!(arguments instanceof JSONArray)) { - String inputTypeError = "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; - result.error("inputTypeError", inputTypeError, null); - break; - } - - JSONArray inputRects = (JSONArray) arguments; - ArrayList decodedRects = decodeExclusionRects(inputRects); - platformMessageHandler.setSystemGestureExclusionRects(decodedRects); - result.success(null); - break; - case "SystemGestures.getSystemGestureExclusionRects": { - List exclusionRects = platformMessageHandler.getSystemGestureExclusionRects(); - if (exclusionRects == null) { - String incorrectApiLevel = "Exclusion rects only exist for Android API 29+."; - result.error("error", incorrectApiLevel, null); - break; - } - - ArrayList> encodedExclusionRects = encodeExclusionRects(exclusionRects); - result.success(encodedExclusionRects); - break; - } - default: - result.notImplemented(); - break; - } - } catch (JSONException e) { - result.error("error", "JSON error: " + e.getMessage(), null); - } - } - } - public enum Brightness { LIGHT("Brightness.light"), DARK("Brightness.dark"); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java index 93b06c2e961ac..12473426f89bf 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java @@ -28,95 +28,97 @@ @RunWith(RobolectricTestRunner.class) public class PlatformChannelTest { @Test - public void setSystemExclusionRectsSendsSuccessMessageToFramework() throws JSONException { + public void itSendsSuccessMessageToFrameworkWhenGettingSystemGestureExclusionRects() throws JSONException { + // --- Test Setup --- DartExecutor dartExecutor = mock(DartExecutor.class); PlatformChannel platformChannel = new PlatformChannel(dartExecutor); PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); platformChannel.setPlatformMessageHandler(platformMessageHandler); + Result result = mock(Result.class); - int top = 0; - int right = 500; - int bottom = 250; - int left = 0; - - ResultsMock resultsMock = mock(ResultsMock.class); - JSONObject JsonRect = new JSONObject(); - JsonRect.put("top", top); - JsonRect.put("right", right); - JsonRect.put("bottom", bottom); - JsonRect.put("left", left); - JSONArray inputRects = new JSONArray(); - inputRects.put(JsonRect); - - ArrayList expectedDecodedRects = new ArrayList(); - Rect gestureRect = new Rect(left, top, right, bottom); - expectedDecodedRects.add(gestureRect); + // Fake API output setup + ArrayList fakeExclusionRects = new ArrayList(); + Rect gestureRect = new Rect(0, 0, 500, 250); + fakeExclusionRects.add(gestureRect); + when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(fakeExclusionRects); - MethodCall callSetSystemGestureExclusionRects = new MethodCall( - "SystemGestures.setSystemGestureExclusionRects", - inputRects + // Parsed API output that should be passed to result.success() + ArrayList> expectedEncodedOutputRects = new ArrayList>(); + HashMap rectMap = new HashMap(); + rectMap.put("top", 0); + rectMap.put("right", 500); + rectMap.put("bottom", 250); + rectMap.put("left", 0); + expectedEncodedOutputRects.add(rectMap); + MethodCall callGetSystemGestureExclusionRects = new MethodCall( + "SystemGestures.getSystemGestureExclusionRects", + null ); - platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, resultsMock); - verify(platformMessageHandler, times(1)).setSystemGestureExclusionRects(expectedDecodedRects); - verify(resultsMock, times(1)).success(null); + // --- Execute Test --- + platformChannel.parsingMethodCallHandler.onMethodCall(callGetSystemGestureExclusionRects, result); + + // --- Verify Results --- + verify(result, times(1)).success(expectedEncodedOutputRects); } @Test - public void setSystemExclusionRectsRequiresJSONArrayInput() { + public void itSendsAPILevelErrorWhenAndroidVersionIsTooLowWhenGettingSystemGestureExclusionRects() { + // --- Test Setup --- DartExecutor dartExecutor = mock(DartExecutor.class); PlatformChannel platformChannel = new PlatformChannel(dartExecutor); PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); platformChannel.setPlatformMessageHandler(platformMessageHandler); + when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(null); + Result result = mock(Result.class); - ResultsMock resultsMock = mock(ResultsMock.class); - String nonJsonInput = "Non-JSON"; - MethodCall callSetSystemGestureExclusionRects = new MethodCall( - "SystemGestures.setSystemGestureExclusionRects", - nonJsonInput + MethodCall callGetSystemGestureExclusionRects = new MethodCall( + "SystemGestures.getSystemGestureExclusionRects", + null ); - platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, resultsMock); - String inputTypeError = "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; - verify(resultsMock, times(1)).error( - "inputTypeError", - inputTypeError, + // --- Execute Test --- + platformChannel.parsingMethodCallHandler.onMethodCall(callGetSystemGestureExclusionRects, result); + + // --- Verify Results --- + verify(result, times(1)).error( + "error", + "Exclusion rects only exist for Android API 29+.", null ); } @Test - public void setSystemExclusionRectsSendsJSONExceptionOnIncorrectDataShape() throws JSONException { + public void itSendsSuccessMessageToFrameworkWhenSettingSystemGestureExclusionRects() throws JSONException { + // --- Test Setup --- DartExecutor dartExecutor = mock(DartExecutor.class); PlatformChannel platformChannel = new PlatformChannel(dartExecutor); PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); platformChannel.setPlatformMessageHandler(platformMessageHandler); + Result result = mock(Result.class); - int top = 0; - int right = 500; - - ResultsMock resultsMock = mock(ResultsMock.class); - JSONObject jsonObject = new JSONObject(); - jsonObject.put("arg1", top); - jsonObject.put("arg2", right); - JSONArray inputArray = new JSONArray(); - inputArray.put(jsonObject); + JSONObject jsonRect = new JSONObject(); + jsonRect.put("top", 0); + jsonRect.put("right", 500); + jsonRect.put("bottom", 250); + jsonRect.put("left", 0); + JSONArray jsonExclusionRectsFromPlatform = new JSONArray(); + jsonExclusionRectsFromPlatform.put(jsonRect); - MethodCall callSetSystemGestureExclusionRects = new MethodCall( + MethodCall callSystemGestureExclusionRects = new MethodCall( "SystemGestures.setSystemGestureExclusionRects", - inputArray - ); - platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, resultsMock); - verify(resultsMock, times(1)).error( - "error", - "JSON error: Incorrect JSON data shape. To set system gesture exclusion rects, \n" + - "a JSONObject with top, right, bottom and left values need to be set to int values.", - null + jsonExclusionRectsFromPlatform ); + + // --- Execute Test --- + platformChannel.parsingMethodCallHandler.onMethodCall(callSystemGestureExclusionRects, result); + + // --- Verify Results --- + verify(result, times(1)).success(null); } @Test - public void itSendsSuccessMessageToFrameworkWhenGettingSystemGestureExclusionRects() throws JSONException { + public void itProperlyDecodesGestureRectsWhenSettingSystemGestureExclusionRects() throws JSONException { // --- Test Setup --- DartExecutor dartExecutor = mock(DartExecutor.class); PlatformChannel platformChannel = new PlatformChannel(dartExecutor); @@ -124,66 +126,87 @@ public void itSendsSuccessMessageToFrameworkWhenGettingSystemGestureExclusionRec platformChannel.setPlatformMessageHandler(platformMessageHandler); Result result = mock(Result.class); - // Fake API output setup - ArrayList fakeExclusionRects = new ArrayList(); + JSONObject jsonRect = new JSONObject(); + jsonRect.put("top", 0); + jsonRect.put("right", 500); + jsonRect.put("bottom", 250); + jsonRect.put("left", 0); + JSONArray jsonExclusionRectsFromPlatform = new JSONArray(); + jsonExclusionRectsFromPlatform.put(jsonRect); + + ArrayList expectedDecodedRects = new ArrayList(); Rect gestureRect = new Rect(0, 0, 500, 250); - fakeExclusionRects.add(gestureRect); - when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(fakeExclusionRects); + expectedDecodedRects.add(gestureRect); - // Parsed API output that should be passed to result.success() - ArrayList> expectedEncodedOutputRects = new ArrayList>(); - HashMap rectMap = new HashMap(); - rectMap.put("top", 0); - rectMap.put("right", 500); - rectMap.put("bottom", 250); - rectMap.put("left", 0); - expectedEncodedOutputRects.add(rectMap); - MethodCall callGetSystemGestureExclusionRects = new MethodCall( - "SystemGestures.getSystemGestureExclusionRects", - null + MethodCall callSetSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + jsonExclusionRectsFromPlatform ); // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall(callGetSystemGestureExclusionRects, result); + platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, result); // --- Verify Results --- - verify(result, times(1)).success(expectedEncodedOutputRects); + verify(platformMessageHandler, times(1)).setSystemGestureExclusionRects(expectedDecodedRects); } @Test - public void itSendsAPILevelErrorWhenAndroidVersionIsTooLowWhenGettingSystemGestureExclusionRects() { + public void itSendsJSONInputErrorWhenNonJSONInputIsUsedWhenSettingSystemGestureExclusionRects() { // --- Test Setup --- DartExecutor dartExecutor = mock(DartExecutor.class); PlatformChannel platformChannel = new PlatformChannel(dartExecutor); PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); platformChannel.setPlatformMessageHandler(platformMessageHandler); - when(platformMessageHandler.getSystemGestureExclusionRects()).thenReturn(null); Result result = mock(Result.class); - MethodCall callGetSystemGestureExclusionRects = new MethodCall( - "SystemGestures.getSystemGestureExclusionRects", - null + String nonJsonInput = "Non-JSON"; + MethodCall callSetSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + nonJsonInput ); // --- Execute Test --- - platformChannel.parsingMethodCallHandler.onMethodCall(callGetSystemGestureExclusionRects, result); + platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, result); // --- Verify Results --- + String inputTypeError = "Input type is incorrect. Ensure that a List> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects."; verify(result, times(1)).error( - "error", - "Exclusion rects only exist for Android API 29+.", + "inputTypeError", + inputTypeError, null ); } - private class ResultsMock implements Result { - @Override - public void success(Object result) {} + @Test + public void itSendsJSONErrorWhenIncorrectJSONShapeIsUsedWhenSettingSystemGestureExclusionRects() throws JSONException { + // --- Test Setup --- + DartExecutor dartExecutor = mock(DartExecutor.class); + PlatformChannel platformChannel = new PlatformChannel(dartExecutor); + PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); + platformChannel.setPlatformMessageHandler(platformMessageHandler); + Result result = mock(Result.class); + + // Add key/value pairs that aren't needed by exclusion rects to simulate incorrect JSON shape + JSONObject jsonObject = new JSONObject(); + jsonObject.put("arg1", 0); + jsonObject.put("arg2", 500); + JSONArray inputArray = new JSONArray(); + inputArray.put(jsonObject); + + MethodCall callSetSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + inputArray + ); - @Override - public void error(String errorCode, String errorMessage, Object errorDetails) {} + // --- Execute Test --- + platformChannel.parsingMethodCallHandler.onMethodCall(callSetSystemGestureExclusionRects, result); - @Override - public void notImplemented() {} + // --- Verify Results --- + verify(result, times(1)).error( + "error", + "JSON error: Incorrect JSON data shape. To set system gesture exclusion rects, \n" + + "a JSONObject with top, right, bottom and left values need to be set to int values.", + null + ); } }