Skip to content

Commit e986f43

Browse files
author
Emmanuel Garcia
authored
Handle SurfaceView in a VirtualDisplay (flutter#33599)
1 parent c40c1a8 commit e986f43

39 files changed

+2300
-246
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
14781478
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
14791479
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
14801480
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
1481+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
1482+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
14811483
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
14821484
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java
14831485
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java

shell/platform/android/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ android_java_sources = [
286286
"io/flutter/plugin/platform/PlatformViewWrapper.java",
287287
"io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java",
288288
"io/flutter/plugin/platform/PlatformViewsController.java",
289+
"io/flutter/plugin/platform/SingleViewPresentation.java",
290+
"io/flutter/plugin/platform/VirtualDisplayController.java",
289291
"io/flutter/util/PathUtils.java",
290292
"io/flutter/util/Preconditions.java",
291293
"io/flutter/util/Predicate.java",

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,21 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
873873
return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs);
874874
}
875875

876+
/**
877+
* Allows a {@code View} that is not currently the input connection target to invoke commands on
878+
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
879+
*
880+
* <p>Returns true to allow non-input-connection-targets to invoke methods on {@code
881+
* InputMethodManager}, or false to exclusively allow the input connection target to invoke such
882+
* methods.
883+
*/
884+
@Override
885+
public boolean checkInputConnectionProxy(View view) {
886+
return flutterEngine != null
887+
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
888+
: super.checkInputConnectionProxy(view);
889+
}
890+
876891
/**
877892
* Invoked when a hardware key is pressed or released.
878893
*

shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,18 @@ private void resize(@NonNull MethodCall call, @NonNull MethodChannel.Result resu
147147
(double) resizeArgs.get("width"),
148148
(double) resizeArgs.get("height"));
149149
try {
150-
final PlatformViewBufferSize sz = handler.resize(resizeRequest);
151-
if (sz == null) {
152-
result.error("error", "Failed to resize the platform view", null);
153-
} else {
154-
final Map<String, Object> response = new HashMap<>();
155-
response.put("width", (double) sz.width);
156-
response.put("height", (double) sz.height);
157-
result.success(response);
158-
}
150+
handler.resize(
151+
resizeRequest,
152+
(PlatformViewBufferSize bufferSize) -> {
153+
if (bufferSize == null) {
154+
result.error("error", "Failed to resize the platform view", null);
155+
} else {
156+
final Map<String, Object> response = new HashMap<>();
157+
response.put("width", (double) bufferSize.width);
158+
response.put("height", (double) bufferSize.height);
159+
result.success(response);
160+
}
161+
});
159162
} catch (IllegalStateException exception) {
160163
result.error("error", detailedExceptionString(exception), null);
161164
}
@@ -298,9 +301,11 @@ public interface PlatformViewsHandler {
298301
* The Flutter application would like to resize an existing Android {@code View}.
299302
*
300303
* @param request The request to resize the platform view.
301-
* @return The buffer size where the platform view pixels are written to.
304+
* @param onComplete Once the resize is completed, this is the handler to notify the size of the
305+
* platform view buffer.
302306
*/
303-
PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request);
307+
void resize(
308+
@NonNull PlatformViewResizeRequest request, @NonNull PlatformViewBufferResized onComplete);
304309

305310
/**
306311
* The Flutter application would like to change the offset of an existing Android {@code View}.
@@ -418,6 +423,11 @@ public PlatformViewBufferSize(int width, int height) {
418423
}
419424
}
420425

426+
/** Allows to notify when a platform view buffer has been resized. */
427+
public interface PlatformViewBufferResized {
428+
void run(@Nullable PlatformViewBufferSize bufferSize);
429+
}
430+
421431
/** The state of a touch event in Flutter within a platform view. */
422432
public static class PlatformViewTouch {
423433
/** The ID of the platform view as seen by the Flutter side. */

shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
8989
try {
9090
final JSONObject arguments = (JSONObject) args;
9191
final int platformViewId = arguments.getInt("platformViewId");
92-
textInputMethodHandler.setPlatformViewClient(platformViewId);
92+
final boolean usesVirtualDisplay =
93+
arguments.optBoolean("usesVirtualDisplay", false);
94+
textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
9395
result.success(null);
9496
} catch (JSONException exception) {
9597
result.error("error", exception.getMessage(), null);
@@ -401,8 +403,10 @@ public interface TextInputMethodHandler {
401403
* different client is set.
402404
*
403405
* @param id the ID of the platform view to be set as a text input client.
406+
* @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses
407+
* hybrid composition.
404408
*/
405-
void setPlatformViewClient(int id);
409+
void setPlatformViewClient(int id, boolean usesVirtualDisplay);
406410

407411
/**
408412
* Sets the size and the transform matrix of the current text input client.

shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
5454
// Initialize the "last seen" text editing values to a non-null value.
5555
private TextEditState mLastKnownFrameworkTextEditingState;
5656

57+
// When true following calls to createInputConnection will return the cached lastInputConnection
58+
// if the input
59+
// target is a platform view. See the comments on lockPlatformViewInputConnection for more
60+
// details.
61+
private boolean isInputConnectionLocked;
62+
5763
@SuppressLint("NewApi")
5864
public TextInputPlugin(
5965
@NonNull View view,
@@ -99,7 +105,7 @@ public void show() {
99105

100106
@Override
101107
public void hide() {
102-
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
108+
if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
103109
notifyViewExited();
104110
} else {
105111
hideTextInput(mView);
@@ -130,8 +136,8 @@ public void setClient(
130136
}
131137

132138
@Override
133-
public void setPlatformViewClient(int platformViewId) {
134-
setPlatformViewTextInputClient(platformViewId);
139+
public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
140+
setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
135141
}
136142

137143
@Override
@@ -176,6 +182,36 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() {
176182
return imeSyncCallback;
177183
}
178184

185+
/**
186+
* Use the current platform view input connection until unlockPlatformViewInputConnection is
187+
* called.
188+
*
189+
* <p>The current input connection instance is cached and any following call to @{link
190+
* createInputConnection} returns the cached connection until unlockPlatformViewInputConnection is
191+
* called.
192+
*
193+
* <p>This is a no-op if the current input target isn't a platform view.
194+
*
195+
* <p>This is used to preserve an input connection when moving a platform view from one virtual
196+
* display to another.
197+
*/
198+
public void lockPlatformViewInputConnection() {
199+
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
200+
isInputConnectionLocked = true;
201+
}
202+
}
203+
204+
/**
205+
* Unlocks the input connection.
206+
*
207+
* <p>See also: @{link lockPlatformViewInputConnection}.
208+
*/
209+
public void unlockPlatformViewInputConnection() {
210+
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
211+
isInputConnectionLocked = false;
212+
}
213+
}
214+
179215
/**
180216
* Detaches the text input plugin from the platform views controller.
181217
*
@@ -259,10 +295,21 @@ public InputConnection createInputConnection(
259295
return null;
260296
}
261297

262-
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
298+
if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
263299
return null;
264300
}
265301

302+
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
303+
if (isInputConnectionLocked) {
304+
return lastInputConnection;
305+
}
306+
lastInputConnection =
307+
platformViewsController
308+
.getPlatformViewById(inputTarget.id)
309+
.onCreateInputConnection(outAttrs);
310+
return lastInputConnection;
311+
}
312+
266313
outAttrs.inputType =
267314
inputTypeFromTextInputType(
268315
configuration.inputType,
@@ -317,7 +364,9 @@ public InputConnection getLastInputConnection() {
317364
* input connection.
318365
*/
319366
public void clearPlatformViewClient(int platformViewId) {
320-
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
367+
if ((inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW
368+
|| inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW)
369+
&& inputTarget.id == platformViewId) {
321370
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
322371
notifyViewExited();
323372
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
@@ -378,13 +427,26 @@ void setTextInputClient(int client, TextInputChannel.Configuration configuration
378427
// setTextInputClient will be followed by a call to setTextInputEditingState.
379428
// Do a restartInput at that time.
380429
mRestartInputPending = true;
430+
unlockPlatformViewInputConnection();
381431
lastClientRect = null;
382432
mEditable.addEditingStateListener(this);
383433
}
384434

385-
private void setPlatformViewTextInputClient(int platformViewId) {
386-
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
387-
lastInputConnection = null;
435+
private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
436+
if (usesVirtualDisplay) {
437+
// We need to make sure that the Flutter view is focused so that no imm operations get short
438+
// circuited.
439+
// Not asking for focus here specifically manifested in a bug on API 28 devices where the
440+
// platform view's request to show a keyboard was ignored.
441+
mView.requestFocus();
442+
inputTarget = new InputTarget(InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW, platformViewId);
443+
mImm.restartInput(mView);
444+
mRestartInputPending = false;
445+
} else {
446+
inputTarget =
447+
new InputTarget(InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW, platformViewId);
448+
lastInputConnection = null;
449+
}
388450
}
389451

390452
private static boolean composingChanged(
@@ -475,10 +537,28 @@ public void inspect(double x, double y) {
475537

476538
@VisibleForTesting
477539
void clearTextInputClient() {
540+
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
541+
// This only applies to platform views that use a virtual display.
542+
// Focus changes in the framework tree have no guarantees on the order focus nodes are
543+
// notified. A node that lost focus may be notified before or after a node that gained focus.
544+
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the
545+
// Flutter text field's focus node will be notified that it lost focus after the AndroidView
546+
// was notified that it gained focus. When this happens the text field will send a
547+
// clearTextInput command which we ignore.
548+
// By doing this we prevent the framework from clearing a platform view input client (the only
549+
// way to do so is to set a new framework text client). I don't see an obvious use case for
550+
// "clearing" a platform view's text input client, and it may be error prone as we don't know
551+
// how the platform view manages the input connection and we probably shouldn't interfere.
552+
// If we ever want to allow the framework to clear a platform view text client we should
553+
// probably consider changing the focus manager such that focus nodes that lost focus are
554+
// notified before focus nodes that gained focus as part of the same focus event.
555+
return;
556+
}
478557
mEditable.removeEditingStateListener(this);
479558
notifyViewExited();
480559
updateAutofillConfigurationIfNeeded(null);
481560
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
561+
unlockPlatformViewInputConnection();
482562
lastClientRect = null;
483563
}
484564

@@ -488,9 +568,12 @@ enum Type {
488568
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
489569
// framework.
490570
FRAMEWORK_CLIENT,
491-
// InputConnection is managed by a platform view that is embeded in the Android view
492-
// hierarchy.
493-
PLATFORM_VIEW,
571+
// InputConnection is managed by a platform view that is presented on a virtual display.
572+
VIRTUAL_DISPLAY_PLATFORM_VIEW,
573+
// InputConnection is managed by a platform view that is embedded in the activity's view
574+
// hierarchy. This view hierarchy is displayed in a physical display within the aplication
575+
// display area.
576+
PHYSICAL_DISPLAY_PLATFORM_VIEW,
494577
}
495578

496579
public InputTarget(@NonNull Type type, int id) {

shell/platform/android/io/flutter/plugin/platform/PlatformView.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,16 @@ default void onFlutterViewDetached() {}
6666
*
6767
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
6868
* connection. This probably doesn't need to be implemented.
69-
*
70-
* <p>This method is deprecated, and will be removed in a future release.
7169
*/
7270
@SuppressLint("NewApi")
73-
@Deprecated
7471
default void onInputConnectionLocked() {}
7572

7673
/**
7774
* Callback fired when the platform input connection has been unlocked.
7875
*
7976
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
8077
* connection. This probably doesn't need to be implemented.
81-
*
82-
* <p>This method is deprecated, and will be removed in a future release.
8378
*/
8479
@SuppressLint("NewApi")
85-
@Deprecated
8680
default void onInputConnectionUnlocked() {}
8781
}

shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public PlatformViewFactory(@Nullable MessageCodec<Object> createArgsCodec) {
2828
* null, or no arguments were sent from the Flutter app.
2929
*/
3030
@NonNull
31-
public abstract PlatformView create(@Nullable Context context, int viewId, @Nullable Object args);
31+
public abstract PlatformView create(Context context, int viewId, @Nullable Object args);
3232

3333
/** Returns the codec to be used for decoding the args parameter of {@link #create}. */
3434
@Nullable

shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public interface PlatformViewsAccessibilityDelegate {
1818
@Nullable
1919
View getPlatformViewById(int viewId);
2020

21+
/** Returns true if the platform view uses virtual displays. */
22+
boolean usesVirtualDisplay(int id);
23+
2124
/**
2225
* Attaches an accessibility bridge for this platform views accessibility delegate.
2326
*

0 commit comments

Comments
 (0)