diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 3d07b6a2cf58d..722001e437a80 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -406,6 +406,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/android/FlutterActivityTest.java", "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", + "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java", "test/io/flutter/util/PreconditionsTest.java", ] 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 68eeb7c7506cc..0ad84d74db89b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -5,8 +5,10 @@ package io.flutter.embedding.engine.systemchannels; import android.content.pm.ActivityInfo; +import android.graphics.Rect; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import org.json.JSONArray; import org.json.JSONException; @@ -32,129 +34,9 @@ public class PlatformChannel { public final MethodChannel channel; @Nullable private PlatformMessageHandler platformMessageHandler; - - private 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 "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); - } - } - }; + @NonNull + @VisibleForTesting + protected final PlatformMethodCallHandler parsingMethodCallHandler = new PlatformMethodCallHandler(); /** * Constructs a {@code PlatformChannel} that connects Android to the Dart code @@ -257,6 +139,40 @@ private int decodeOrientations(@NonNull JSONArray encodedOrientations) throws JS return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } + /** + * Decodes a JSONArray of rectangle data into an ArrayList. + * + * @throws JSONException if {@code inputRects} does not contain expected keys and value types. + */ + @NonNull + private ArrayList decodeRects(@NonNull JSONArray inputRects) throws JSONException { + ArrayList exclusionRects = new ArrayList(); + for (int i = 0; i < inputRects.length(); i++) { + JSONObject rect = inputRects.getJSONObject(i); + int top; + int right; + int bottom; + int left; + + try { + top = rect.getInt("top"); + right = rect.getInt("right"); + bottom = rect.getInt("bottom"); + left = rect.getInt("left"); + } catch (JSONException exception) { + throw new JSONException( + "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." + ); + } + + Rect gestureRect = new Rect(left, top, right, bottom); + exclusionRects.add(gestureRect); + } + + return exclusionRects; + } + @NonNull private AppSwitcherDescription decodeAppSwitcherDescription(@NonNull JSONObject encodedDescription) throws JSONException { int color = encodedDescription.getInt("primaryColor"); @@ -420,6 +336,12 @@ public interface PlatformMessageHandler { * clipboard to the given {@code text}. */ void setClipboardData(@NonNull String text); + + /** + * The Flutter application would like to set the system gesture exclusion + * rects through the given {@code rects}. + */ + void setSystemGestureExclusionRects(@NonNull ArrayList rects); } /** @@ -581,6 +503,147 @@ 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 "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 = decodeRects(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); + } + } + } + public enum Brightness { LIGHT("Brightness.light"), DARK("Brightness.dark"); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index 0a9718a81c0b1..7a9c9c8059867 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -9,6 +9,7 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.graphics.Rect; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -17,6 +18,8 @@ import android.view.View; import android.view.Window; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import io.flutter.embedding.engine.systemchannels.PlatformChannel; @@ -84,6 +87,11 @@ public CharSequence getClipboardData(@Nullable PlatformChannel.ClipboardContentF public void setClipboardData(@NonNull String text) { PlatformPlugin.this.setClipboardData(text); } + + @Override + public void setSystemGestureExclusionRects(@NonNull ArrayList rects) { + PlatformPlugin.this.setSystemGestureExclusionRects(rects); + } }; public PlatformPlugin(Activity activity, PlatformChannel platformChannel) { @@ -272,4 +280,14 @@ private void setClipboardData(String text) { ClipData clip = ClipData.newPlainText("text label?", text); clipboard.setPrimaryClip(clip); } + + private void setSystemGestureExclusionRects(ArrayList rects) { + if (Build.VERSION.SDK_INT < 29) { + return; + } + + Window window = activity.getWindow(); + View view = window.getDecorView(); + view.setSystemGestureExclusionRects(rects); + } } diff --git a/shell/platform/android/test/README.md b/shell/platform/android/test/README.md index 8d72cef9396ff..88f7fc5c59d33 100644 --- a/shell/platform/android/test/README.md +++ b/shell/platform/android/test/README.md @@ -18,7 +18,7 @@ integration tests in other repos. 3. Add your class to the `@SuiteClasses` annotation in `FlutterTestSuite.java`. This makes sure the test is actually executed at run time. 4. Write your test. -5. Build and run with `testing/run_tests.py [--type=java] [filter=]`. +5. Build and run with `testing/run_tests.py [--type=java] [--java-filter=]`. ## Q&A @@ -84,7 +84,7 @@ Tags: robolectric_version: ``` -Then update the `DEPS` file (located at /src/flutter/DEPS) to use the new version by pointing to +Then update the `DEPS` file (located at /src/flutter/DEPS) to use the new version by pointing to your new `last_updated_at` tag. ``` diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 6c95754ccb402..ffc41acac88d5 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -13,6 +13,7 @@ import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; import io.flutter.embedding.engine.systemchannels.TextInputChannelTest; +import io.flutter.embedding.engine.systemchannels.PlatformChannelTest; import io.flutter.util.PreconditionsTest; @RunWith(Suite.class) @@ -23,6 +24,7 @@ FlutterFragmentTest.class, // FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this FlutterEngineCacheTest.class, + PlatformChannelTest.class, TextInputChannelTest.class }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ 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 new file mode 100644 index 0000000000000..31d7d71c39fd8 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java @@ -0,0 +1,126 @@ +package io.flutter.embedding.engine.systemchannels; + +import android.graphics.Rect; + +import java.util.ArrayList; + +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.PlatformMessageHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.MethodCall; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class PlatformChannelTest { + @Test + public void setSystemExclusionRectsSendsSuccessMessageToFramework() throws JSONException { + DartExecutor dartExecutor = mock(DartExecutor.class); + PlatformChannel platformChannel = new PlatformChannel(dartExecutor); + PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); + platformChannel.setPlatformMessageHandler(platformMessageHandler); + + 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); + + MethodCall callSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + inputRects + ); + + platformChannel.parsingMethodCallHandler.onMethodCall(callSystemGestureExclusionRects, resultsMock); + verify(platformMessageHandler, times(1)).setSystemGestureExclusionRects(expectedDecodedRects); + verify(resultsMock, times(1)).success(null); + } + + @Test + public void setSystemExclusionRectsRequiresJSONArrayInput() { + DartExecutor dartExecutor = mock(DartExecutor.class); + PlatformChannel platformChannel = new PlatformChannel(dartExecutor); + PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); + platformChannel.setPlatformMessageHandler(platformMessageHandler); + + ResultsMock resultsMock = mock(ResultsMock.class); + String nonJsonInput = "Non-JSON"; + MethodCall callSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + nonJsonInput + ); + platformChannel.parsingMethodCallHandler.onMethodCall(callSystemGestureExclusionRects, 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, + null + ); + } + + @Test + public void setSystemExclusionRectsSendsJSONExceptionOnIncorrectDataShape() throws JSONException { + DartExecutor dartExecutor = mock(DartExecutor.class); + PlatformChannel platformChannel = new PlatformChannel(dartExecutor); + PlatformMessageHandler platformMessageHandler = mock(PlatformMessageHandler.class); + platformChannel.setPlatformMessageHandler(platformMessageHandler); + + 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); + + MethodCall callSystemGestureExclusionRects = new MethodCall( + "SystemGestures.setSystemGestureExclusionRects", + inputArray + ); + platformChannel.parsingMethodCallHandler.onMethodCall(callSystemGestureExclusionRects, 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 + ); + } + + private class ResultsMock implements Result { + @Override + public void success(Object result) {} + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) {} + + @Override + public void notImplemented() {} + } +}