Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 74b74b1

Browse files
itsjustkevindkwingsmtcbrackenEmmanuel Garciajason-simmons
authored
[flutter_releases] Flutter beta 2.10.0-0.2.pre Engine Cherrypicks (#30918)
* Impl and test (#30488) * Fix missing shcore.dll error on Windows 7 (#30699) * Remove glitch when displaying platform views (#30724) * Win32: Fix Korean text input (#30805) Fixes an issue with Korean IMEs wherein a text input state update may be sent to the framework that misleads the framework into assuming that IME composing has ended. When inputting Korean text, characters are built up keystroke by keystroke until the point that either: * the user presses space/enter to terminate composing and commit the character, or; * the user presses a key such that the character currently being composed cannot be modified further, and the IME determines that the user has begun composing the next character. The following is an example sequence of events for the latter case: 1. User presses ㅂ. GCS_COMPSTR event received with ㅂ. Embedder sends state update to framework. 2. User presses ㅏ. GCS_COMPSTR event received with 바. Embedder sends state update to framework. 3. User presses ㄴ. GCS_COMPSTR event received with 반. Embedder sends state update to framework. 4. User presses ㅏ. At this point, the current character being composed (반) cannot be modified in a meaningful way, and the IME determines that the user is typing 바 followed by 나. GCS_RESULTSTR event received with 바, immediately followed by GCS_COMPSTR event with 나. In step 4, we previously sent two events to the framework, one immediately after the other: * GCS_RESULTSTR triggers the text input model to commit the current composing region to the string under edit. This causes the composing region to collapse to an empty range. * GCS_COMPSTR triggers the text input model to insert the new composing character and set the composing region to that character. Conceptually, this is an atomic operation. The fourth keystroke causes the 반 character to be broken into two (바 and ㄴ) and the latter to be modified to 나. From the user's point of view, as well as from the IME's point of view, the user has NOT stopped composing, and the composing region has simply moved on to the next character. Flutter has no concept of whether the user is composing or not other that whether a non-empty composing region exists. As such, sending a state update after the GCS_RESULTSTR event misleads the framework into believing that composing has ended. This triggers a serious bug: Text fields with input formatters applied do not perform input formatting updates while composing is active; instead they wait until composing has ended to apply any formatting. The previous behaviour would thus trigger input formatters to be applied each time the user input caused a new character to be input. This has the add-on negative effect that once formatting has been applied, it sends an update back to the embedder so that the native OS text input state can be updated. However, since the GCS_RESULTSTR event is _immediately_ followed by a GCS_COMPSTR, the state has changed in the meantime, and the embedder is left processing an update (the intermediate state sent after the GCS_RESULTSTR) which is now out of date (i.e. missing the new state from the GCS_COMPSTR event). Since GCS_RESULTR events are always immediately followed by a subsequent GCS_COMPSTR (in the case where composing continues) or a WM_IME_ENDCOMPOSITION (in the case where composing is finished), and because the event handlers for both of those send updated state to the framework, this change eliminates sending the (intermediate) state in response to GCS_COMPSTR events. Issue: flutter/flutter#96209 (full fix) Issue: flutter/flutter#88645 (partial fix) * Ensure that PlatformViewIOS does not call into Shell semantics APIs during destruction (#30835) * Ensure that PlatformViewIOS does not call into Shell semantics APIs during destruction (#30835) Co-authored-by: Tong Mu <[email protected]> Co-authored-by: Chris Bracken <[email protected]> Co-authored-by: Emmanuel Garcia <[email protected]> Co-authored-by: Jason Simmons <[email protected]>
1 parent 63ca995 commit 74b74b1

File tree

16 files changed

+340
-78
lines changed

16 files changed

+340
-78
lines changed

lib/web_ui/lib/src/engine/keyboard_binding.dart

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,7 @@ class KeyboardConverter {
371371
// followed by an immediate cancel event.
372372
(_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock);
373373

374-
final int? lastLogicalRecord = _pressingRecords[physicalKey];
375-
376-
ui.KeyEventType type;
374+
final ui.KeyEventType type;
377375

378376
if (_shouldSynthesizeCapsLockUp() && event.code! == _kPhysicalCapsLock) {
379377
// Case 1: Handle CapsLock on macOS
@@ -399,28 +397,45 @@ class KeyboardConverter {
399397

400398
} else if (isPhysicalDown) {
401399
// Case 2: Handle key down of normal keys
402-
type = ui.KeyEventType.down;
403-
if (lastLogicalRecord != null) {
400+
if (_pressingRecords[physicalKey] != null) {
404401
// This physical key is being pressed according to the record.
405402
if (event.repeat ?? false) {
406403
// A normal repeated key.
407404
type = ui.KeyEventType.repeat;
408405
} else {
409406
// A non-repeated key has been pressed that has the exact physical key as
410-
// a currently pressed one, usually indicating multiple keyboards are
411-
// pressing keys with the same physical key, or the up event was lost
412-
// during a loss of focus. The down event is ignored.
413-
event.preventDefault();
414-
return;
407+
// a currently pressed one. This can mean one of the following cases:
408+
//
409+
// * Multiple keyboards are pressing keys with the same physical key.
410+
// * The up event was lost during a loss of focus.
411+
// * The previous down event was a system shortcut and its release
412+
// was skipped (see `_startGuardingKey`,) such as holding Ctrl and
413+
// pressing V then V, within the "guard window".
414+
//
415+
// The three cases can't be distinguished, and in the 3rd case, the
416+
// latter event must be dispatched as down events for the framework to
417+
// correctly recognize and choose to not to handle. Therefore, an up
418+
// event is synthesized before it.
419+
_dispatchKeyData!(ui.KeyData(
420+
timeStamp: timeStamp,
421+
type: ui.KeyEventType.up,
422+
physical: physicalKey,
423+
logical: logicalKey,
424+
character: null,
425+
synthesized: true,
426+
));
427+
_pressingRecords.remove(physicalKey);
428+
type = ui.KeyEventType.down;
415429
}
416430
} else {
417431
// This physical key is not being pressed according to the record. It's a
418432
// normal down event, whether the system event is a repeat or not.
433+
type = ui.KeyEventType.down;
419434
}
420435

421436
} else { // isPhysicalDown is false and not CapsLock
422437
// Case 2: Handle key up of normal keys
423-
if (lastLogicalRecord == null) {
438+
if (_pressingRecords[physicalKey] == null) {
424439
// The physical key has been released before. It indicates multiple
425440
// keyboards pressed keys with the same physical key. Ignore the up event.
426441
event.preventDefault();
@@ -430,6 +445,10 @@ class KeyboardConverter {
430445
type = ui.KeyEventType.up;
431446
}
432447

448+
// The _pressingRecords[physicalKey] might have been changed during the last
449+
// `if` clause.
450+
final int? lastLogicalRecord = _pressingRecords[physicalKey];
451+
433452
final int? nextLogicalRecord;
434453
switch (type) {
435454
case ui.KeyEventType.down:

lib/web_ui/test/keyboard_converter_test.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ void testMain() {
378378
converter.handleEvent(keyUpEvent('ShiftLeft', 'Shift', 0, kLocationLeft));
379379
});
380380

381-
test('Duplicate down is ignored', () {
381+
test('Duplicate down is preceded with synthesized up', () {
382382
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
383383
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
384384
keyDataList.add(key);
@@ -392,15 +392,26 @@ void testMain() {
392392
);
393393
expect(preventedDefault, isTrue);
394394
preventedDefault = false;
395-
// A KeyUp of ShiftLeft is missed due to loss of focus.
395+
// A KeyUp of ShiftLeft is missed.
396396

397397
keyDataList.clear();
398398
converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)
399399
..onPreventDefault = onPreventDefault
400400
);
401-
expect(keyDataList, hasLength(1));
402-
expect(keyDataList[0].physical, 0);
403-
expect(keyDataList[0].logical, 0);
401+
expect(keyDataList, hasLength(2));
402+
expectKeyData(keyDataList.first,
403+
type: ui.KeyEventType.up,
404+
physical: kPhysicalShiftLeft,
405+
logical: kLogicalShiftLeft,
406+
character: null,
407+
synthesized: true,
408+
);
409+
expectKeyData(keyDataList.last,
410+
type: ui.KeyEventType.down,
411+
physical: kPhysicalShiftLeft,
412+
logical: kLogicalShiftLeft,
413+
character: null,
414+
);
404415
expect(preventedDefault, isTrue);
405416

406417
keyDataList.clear();

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class FlutterSurfaceView extends SurfaceView implements RenderSurface {
3737

3838
private final boolean renderTransparently;
3939
private boolean isSurfaceAvailableForRendering = false;
40+
private boolean isPaused = false;
4041
private boolean isAttachedToFlutterRenderer = false;
4142
@Nullable private FlutterRenderer flutterRenderer;
4243

@@ -200,6 +201,7 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) {
200201
"Surface is available for rendering. Connecting FlutterRenderer to Android surface.");
201202
connectSurfaceToRenderer();
202203
}
204+
isPaused = false;
203205
}
204206

205207
/**
@@ -241,6 +243,7 @@ public void pause() {
241243
// Don't remove the `flutterUiDisplayListener` as `onFlutterUiDisplayed()` will make
242244
// the `FlutterSurfaceView` visible.
243245
flutterRenderer = null;
246+
isPaused = true;
244247
isAttachedToFlutterRenderer = false;
245248
} else {
246249
Log.w(TAG, "pause() invoked when no FlutterRenderer was attached.");
@@ -253,8 +256,13 @@ private void connectSurfaceToRenderer() {
253256
throw new IllegalStateException(
254257
"connectSurfaceToRenderer() should only be called when flutterRenderer and getHolder() are non-null.");
255258
}
256-
257-
flutterRenderer.startRenderingToSurface(getHolder().getSurface());
259+
// When connecting the surface to the renderer, it's possible that the surface is currently
260+
// paused. For instance, when a platform view is displayed, the current FlutterSurfaceView
261+
// is paused, and rendering continues in a FlutterImageView buffer while the platform view
262+
// is displayed.
263+
//
264+
// startRenderingToSurface stops rendering to an active surface if it isn't paused.
265+
flutterRenderer.startRenderingToSurface(getHolder().getSurface(), isPaused);
258266
}
259267

260268
// FlutterRenderer must be non-null.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class FlutterTextureView extends TextureView implements RenderSurface {
3636

3737
private boolean isSurfaceAvailableForRendering = false;
3838
private boolean isAttachedToFlutterRenderer = false;
39+
private boolean isPaused = false;
3940
@Nullable private FlutterRenderer flutterRenderer;
4041
@Nullable private Surface renderSurface;
4142

@@ -187,6 +188,7 @@ public void detachFromRenderer() {
187188
public void pause() {
188189
if (flutterRenderer != null) {
189190
flutterRenderer = null;
191+
isPaused = true;
190192
isAttachedToFlutterRenderer = false;
191193
} else {
192194
Log.w(TAG, "pause() invoked when no FlutterRenderer was attached.");
@@ -217,7 +219,8 @@ private void connectSurfaceToRenderer() {
217219
}
218220

219221
renderSurface = new Surface(getSurfaceTexture());
220-
flutterRenderer.startRenderingToSurface(renderSurface);
222+
flutterRenderer.startRenderingToSurface(renderSurface, isPaused);
223+
isPaused = false;
221224
}
222225

223226
// FlutterRenderer must be non-null.

shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public void onEngineWillDestroy() {
134134
*
135135
* <p>A new {@code FlutterEngine} will not display any UI until a {@link RenderSurface} is
136136
* registered. See {@link #getRenderer()} and {@link
137-
* FlutterRenderer#startRenderingToSurface(Surface)}.
137+
* FlutterRenderer#startRenderingToSurface(Surface, boolean)}.
138138
*
139139
* <p>A new {@code FlutterEngine} automatically attaches all plugins. See {@link #getPlugins()}.
140140
*

shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,24 @@ public void run() {
221221
* Notifies Flutter that the given {@code surface} was created and is available for Flutter
222222
* rendering.
223223
*
224+
* <p>If called more than once, the current native resources are released. This can be undesired
225+
* if the Engine expects to reuse this surface later. For example, this is true when platform
226+
* views are displayed in a frame, and then removed in the next frame.
227+
*
228+
* <p>To avoid releasing the current surface resources, set {@code keepCurrentSurface} to true.
229+
*
224230
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
225231
* android.view.TextureView.SurfaceTextureListener}
232+
*
233+
* @param surface The render surface.
234+
* @param keepCurrentSurface True if the current active surface should not be released.
226235
*/
227-
public void startRenderingToSurface(@NonNull Surface surface) {
228-
if (this.surface != null) {
236+
public void startRenderingToSurface(@NonNull Surface surface, boolean keepCurrentSurface) {
237+
// Don't stop rendering the surface if it's currently paused.
238+
// Stop rendering to the surface releases the associated native resources, which
239+
// causes a glitch when showing platform views.
240+
// For more, https://github.com/flutter/flutter/issues/95343
241+
if (this.surface != null && !keepCurrentSurface) {
229242
stopRenderingToSurface();
230243
}
231244

@@ -248,8 +261,8 @@ public void swapSurface(@NonNull Surface surface) {
248261

249262
/**
250263
* Notifies Flutter that a {@code surface} previously registered with {@link
251-
* #startRenderingToSurface(Surface)} has changed size to the given {@code width} and {@code
252-
* height}.
264+
* #startRenderingToSurface(Surface, boolean)} has changed size to the given {@code width} and
265+
* {@code height}.
253266
*
254267
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
255268
* android.view.TextureView.SurfaceTextureListener}
@@ -260,8 +273,8 @@ public void surfaceChanged(int width, int height) {
260273

261274
/**
262275
* Notifies Flutter that a {@code surface} previously registered with {@link
263-
* #startRenderingToSurface(Surface)} has been destroyed and needs to be released and cleaned up
264-
* on the Flutter side.
276+
* #startRenderingToSurface(Surface, boolean)} has been destroyed and needs to be released and
277+
* cleaned up on the Flutter side.
265278
*
266279
* <p>See {@link android.view.SurfaceHolder.Callback} and {@link
267280
* android.view.TextureView.SurfaceTextureListener}

shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public interface RenderSurface {
3737
* FlutterRenderer} at the appropriate times:
3838
*
3939
* <ol>
40-
* <li>{@link FlutterRenderer#startRenderingToSurface(Surface)}
40+
* <li>{@link FlutterRenderer#startRenderingToSurface(Surface, boolean)}
4141
* <li>{@link FlutterRenderer#surfaceChanged(int, int)}}
4242
* <li>{@link FlutterRenderer#stopRenderingToSurface()}
4343
* </ol>

shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static org.mockito.ArgumentMatchers.anyInt;
77
import static org.mockito.ArgumentMatchers.eq;
88
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.never;
910
import static org.mockito.Mockito.times;
1011
import static org.mockito.Mockito.verify;
1112
import static org.mockito.Mockito.when;
@@ -45,7 +46,7 @@ public void itForwardsSurfaceCreationNotificationToFlutterJNI() {
4546
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
4647

4748
// Execute the behavior under test.
48-
flutterRenderer.startRenderingToSurface(fakeSurface);
49+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
4950

5051
// Verify the behavior under test.
5152
verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface));
@@ -57,7 +58,7 @@ public void itForwardsSurfaceChangeNotificationToFlutterJNI() {
5758
Surface fakeSurface = mock(Surface.class);
5859
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
5960

60-
flutterRenderer.startRenderingToSurface(fakeSurface);
61+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
6162

6263
// Execute the behavior under test.
6364
flutterRenderer.surfaceChanged(100, 50);
@@ -72,7 +73,7 @@ public void itForwardsSurfaceDestructionNotificationToFlutterJNI() {
7273
Surface fakeSurface = mock(Surface.class);
7374
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
7475

75-
flutterRenderer.startRenderingToSurface(fakeSurface);
76+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
7677

7778
// Execute the behavior under test.
7879
flutterRenderer.stopRenderingToSurface();
@@ -87,10 +88,10 @@ public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() {
8788
Surface fakeSurface2 = mock(Surface.class);
8889
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
8990

90-
flutterRenderer.startRenderingToSurface(fakeSurface);
91+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
9192

9293
// Execute behavior under test.
93-
flutterRenderer.startRenderingToSurface(fakeSurface2);
94+
flutterRenderer.startRenderingToSurface(fakeSurface2, /*keepCurrentSurface=*/ false);
9495

9596
// Verify behavior under test.
9697
verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); // notification of 1st surface's removal.
@@ -101,7 +102,7 @@ public void itStopsRenderingToSurfaceWhenRequested() {
101102
// Setup the test.
102103
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
103104

104-
flutterRenderer.startRenderingToSurface(fakeSurface);
105+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
105106

106107
// Execute the behavior under test.
107108
flutterRenderer.stopRenderingToSurface();
@@ -110,6 +111,32 @@ public void itStopsRenderingToSurfaceWhenRequested() {
110111
verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
111112
}
112113

114+
@Test
115+
public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() {
116+
// Setup the test.
117+
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
118+
119+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
120+
121+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
122+
123+
// Verify behavior under test.
124+
verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
125+
}
126+
127+
@Test
128+
public void itNeverStopsRenderingToSurfaceWhenRequested() {
129+
// Setup the test.
130+
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
131+
132+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
133+
134+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ true);
135+
136+
// Verify behavior under test.
137+
verify(fakeFlutterJNI, never()).onSurfaceDestroyed();
138+
}
139+
113140
@Test
114141
public void itStopsSurfaceTextureCallbackWhenDetached() {
115142
// Setup the test.
@@ -120,7 +147,7 @@ public void itStopsSurfaceTextureCallbackWhenDetached() {
120147
FlutterRenderer.SurfaceTextureRegistryEntry entry =
121148
(FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture();
122149

123-
flutterRenderer.startRenderingToSurface(fakeSurface);
150+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
124151

125152
// Execute the behavior under test.
126153
flutterRenderer.stopRenderingToSurface();
@@ -143,7 +170,7 @@ public void itRegistersExistingSurfaceTexture() {
143170
(FlutterRenderer.SurfaceTextureRegistryEntry)
144171
flutterRenderer.registerSurfaceTexture(surfaceTexture);
145172

146-
flutterRenderer.startRenderingToSurface(fakeSurface);
173+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
147174

148175
// Verify behavior under test.
149176
assertEquals(surfaceTexture, entry.surfaceTexture());
@@ -164,7 +191,7 @@ public void itUnregistersTextureWhenSurfaceTextureFinalized() {
164191
(FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture();
165192
long id = entry.id();
166193

167-
flutterRenderer.startRenderingToSurface(fakeSurface);
194+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
168195

169196
// Execute the behavior under test.
170197
runFinalization(entry);
@@ -190,7 +217,7 @@ public void itStopsUnregisteringTextureWhenDetached() {
190217
(FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture();
191218
long id = entry.id();
192219

193-
flutterRenderer.startRenderingToSurface(fakeSurface);
220+
flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false);
194221

195222
flutterRenderer.stopRenderingToSurface();
196223

shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.flutter.plugin.platform;
22

3+
import static android.os.Looper.getMainLooper;
34
import static io.flutter.embedding.engine.systemchannels.PlatformViewsChannel.PlatformViewTouch;
45
import static org.junit.Assert.*;
56
import static org.mockito.ArgumentMatchers.*;
67
import static org.mockito.Mockito.*;
8+
import static org.robolectric.Shadows.shadowOf;
79

810
import android.content.Context;
911
import android.content.res.AssetManager;
@@ -486,10 +488,7 @@ public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() {
486488
platformViewsController.onBeginFrame();
487489
platformViewsController.onEndFrame();
488490

489-
verify(overlayImageView, never()).detachFromRenderer();
490-
491-
// Simulate first frame from the framework.
492-
jni.onFirstFrame();
491+
shadowOf(getMainLooper()).idle();
493492
verify(overlayImageView, times(1)).detachFromRenderer();
494493
}
495494

0 commit comments

Comments
 (0)