Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,129 +34,9 @@ public class PlatformChannel {
public final MethodChannel channel;
@Nullable
private PlatformMessageHandler platformMessageHandler;

private final MethodChannel.MethodCallHandler parsingMethodCallHandler = new MethodChannel.MethodCallHandler() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shihaohong why was this moved?

@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<SystemUiOverlay> 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
Expand Down Expand Up @@ -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<Rect>.
*
* @throws JSONException if {@code inputRects} does not contain expected keys and value types.
*/
@NonNull
private ArrayList<Rect> decodeRects(@NonNull JSONArray inputRects) throws JSONException {
ArrayList<Rect> exclusionRects = new ArrayList<Rect>();
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");
Expand Down Expand Up @@ -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<Rect> rects);
}

/**
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like for us to avoid non-static inner classes. I think in this case I'd like for us to revert the movement of this handler definition back to where it was. But in future cases where we do define inner classes, they should be static to avoid the possibility of accidentally leaking references to the outer class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a discussion with @mklim and we felt that it was difficult to read an inline class definition within another class. I'll revert this in a subsequent PR

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish the diff were better here. I spot checked it, assuming that nothing in this was changed from the original besides the new method call.

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<SystemUiOverlay> 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<Map<String, int>> is passed as the input for SystemGestureExclusionRects.setSystemGestureExclusionRects.";
result.error("inputTypeError", inputTypeError, null);
break;
}

JSONArray inputRects = (JSONArray) arguments;
ArrayList<Rect> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -272,4 +280,14 @@ private void setClipboardData(String text) {
ClipData clip = ClipData.newPlainText("text label?", text);
clipboard.setPrimaryClip(clip);
}

private void setSystemGestureExclusionRects(ArrayList<Rect> rects) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the rest of the PR besides this method could be tested, even with the SDK limitations. Even though this is never called, I think you could check that the arguments are parsed safely in PlatformChannel. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense! I'll give it a shot

if (Build.VERSION.SDK_INT < 29) {
return;
}

Window window = activity.getWindow();
View view = window.getDecorView();
view.setSystemGestureExclusionRects(rects);
}
}
Loading