-
Notifications
You must be signed in to change notification settings - Fork 5.9k
/
PlatformViewsController.java
1353 lines (1207 loc) · 55.7 KB
/
PlatformViewsController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugin.platform;
import static android.view.MotionEvent.PointerCoords;
import static android.view.MotionEvent.PointerProperties;
import static io.flutter.Build.API_LEVELS;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.MutableContextWrapper;
import android.os.Build;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.*;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.util.ViewUtils;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Manages platform views.
*
* <p>Each {@link io.flutter.embedding.engine.FlutterEngine} or {@link
* io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A platform views
* controller can be attached to at most one Flutter view.
*/
public class PlatformViewsController implements PlatformViewsAccessibilityDelegate {
private static final String TAG = "PlatformViewsController";
// These view types allow out-of-band drawing commands that don't notify the Android view
// hierarchy.
// To support these cases, Flutter hosts the embedded view in a VirtualDisplay,
// and binds the VirtualDisplay to a GL texture that is then composed by the engine.
// However, there are a few issues with Virtual Displays. For example, they don't fully support
// accessibility due to https://github.com/flutter/flutter/issues/29717,
// and keyboard interactions may have non-deterministic behavior.
// Views that issue out-of-band drawing commands that aren't included in this array are
// required to call `View#invalidate()` to notify Flutter about the update.
// This isn't ideal, but given all the other limitations it's a reasonable tradeoff.
// Related issue: https://github.com/flutter/flutter/issues/103630
private static Class[] VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY = {SurfaceView.class};
private final PlatformViewRegistryImpl registry;
private AndroidTouchProcessor androidTouchProcessor;
// The context of the Activity or Fragment hosting the render target for the Flutter engine.
private Context context;
// The View currently rendering the Flutter UI associated with these platform views.
private FlutterView flutterView;
// The texture registry maintaining the textures into which the embedded views will be rendered.
@Nullable private TextureRegistry textureRegistry;
@Nullable private TextInputPlugin textInputPlugin;
// The system channel used to communicate with the framework about platform views.
private PlatformViewsChannel platformViewsChannel;
// The accessibility bridge to which accessibility events form the platform views will be
// dispatched.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
// TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make
// this private. This is visible as a hack to facilitate testing. This was deemed the least
// bad option at the time of writing.
@VisibleForTesting /* package */ final HashMap<Integer, VirtualDisplayController> vdControllers;
// Maps a virtual display's context to the embedded view hosted in this virtual display.
// Since each virtual display has it's unique context this allows associating any view with the
// platform view that
// it is associated with(e.g if a platform view creates other views in the same virtual display.
@VisibleForTesting /* package */ final HashMap<Context, View> contextToEmbeddedView;
// The platform views.
private final SparseArray<PlatformView> platformViews;
// The platform view wrappers that are appended to FlutterView.
//
// These platform views use a PlatformViewLayer in the framework. This is different than
// the platform views that use a TextureLayer.
//
// This distinction is necessary because a PlatformViewLayer allows to embed Android's
// SurfaceViews in a Flutter app whereas the texture layer is unable to support such native views.
//
// If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't
// in the view hierarchy.
//
// This view provides a wrapper that applies scene builder operations to the platform view.
// For example, a transform matrix, or setting opacity on the platform view layer.
private final SparseArray<FlutterMutatorView> platformViewParent;
// Map of unique IDs to views that render overlay layers.
private final SparseArray<PlatformOverlayView> overlayLayerViews;
// The platform view wrappers that are appended to FlutterView.
//
// These platform views use a TextureLayer in the framework. This is different than
// the platform views that use a PlatformViewLayer.
//
// This is the default mode, and recommended for better performance.
private final SparseArray<PlatformViewWrapper> viewWrappers;
// Next available unique ID for use in overlayLayerViews.
private int nextOverlayLayerId = 0;
// Tracks whether the flutterView has been converted to use a FlutterImageView.
private boolean flutterViewConvertedToImageView = false;
// When adding platform views using Hybrid Composition, the engine converts the render surface
// to a FlutterImageView to help improve animation synchronization on Android. This flag allows
// disabling this conversion through the PlatformView platform channel.
private boolean synchronizeToNativeViewHierarchy = true;
// Overlay layer IDs that were displayed since the start of the current frame.
private final HashSet<Integer> currentFrameUsedOverlayLayerIds;
// Platform view IDs that were displayed since the start of the current frame.
private final HashSet<Integer> currentFrameUsedPlatformViewIds;
// Used to acquire the original motion events using the motionEventIds.
private final MotionEventTracker motionEventTracker;
// Whether software rendering is used.
private boolean usesSoftwareRendering = false;
private static boolean enableImageRenderTarget = true;
private static boolean enableSurfaceProducerRenderTarget = true;
private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
new PlatformViewsChannel.PlatformViewsHandler() {
@Override
// TODO(egarciad): Remove the need for this.
// https://github.com/flutter/flutter/issues/96679
public void createForPlatformViewLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// API level 19 is required for `android.graphics.ImageReader`.
enforceMinimumAndroidApiVersion(19);
ensureValidRequest(request);
final PlatformView platformView = createPlatformView(request, false);
configureForHybridComposition(platformView, request);
// New code should be added to configureForHybridComposition, not here, unless it is
// not applicable to fallback from TLHC to HC.
}
@Override
public long createForTextureLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
ensureValidRequest(request);
final int viewId = request.viewId;
if (viewWrappers.get(viewId) != null) {
throw new IllegalStateException(
"Trying to create an already created platform view, view id: " + viewId);
}
if (textureRegistry == null) {
throw new IllegalStateException(
"Texture registry is null. This means that platform views controller was detached,"
+ " view id: "
+ viewId);
}
if (flutterView == null) {
throw new IllegalStateException(
"Flutter view is null. This means the platform views controller doesn't have an"
+ " attached view, view id: "
+ viewId);
}
final PlatformView platformView = createPlatformView(request, true);
final View embeddedView = platformView.getView();
if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a"
+ " parent view.");
}
// The newer Texture Layer Hybrid Composition mode isn't suppported if any of the
// following are true:
// - The embedded view contains any of the VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY view types.
// These views allow out-of-band graphics operations that aren't notified to the Android
// view hierarchy via callbacks such as ViewParent#onDescendantInvalidated().
// - The API level is <23, due to TLHC implementation API requirements.
final boolean supportsTextureLayerMode =
Build.VERSION.SDK_INT >= API_LEVELS.API_23
&& !ViewUtils.hasChildViewOfType(
embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY);
// Fall back to Hybrid Composition or Virtual Display when necessary, depending on which
// fallback mode is requested.
if (!supportsTextureLayerMode) {
if (request.displayMode
== PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode
.TEXTURE_WITH_HYBRID_FALLBACK) {
configureForHybridComposition(platformView, request);
return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK;
} else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode.
return configureForVirtualDisplay(platformView, request);
}
// TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change.
// For now, preserve the 3.0 behavior of falling through to Texture Layer mode even
// though it won't work correctly.
}
return configureForTextureLayerComposition(platformView, request);
}
@Override
public void dispose(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Disposing unknown platform view with id: " + viewId);
return;
}
if (platformView.getView() != null) {
final View embeddedView = platformView.getView();
final ViewGroup pvParent = (ViewGroup) embeddedView.getParent();
if (pvParent != null) {
// Eagerly remove the embedded view from the PlatformViewWrapper.
// Without this call, we see some crashes because removing the view
// is used as a signal to stop processing.
pvParent.removeView(embeddedView);
}
}
platformViews.remove(viewId);
try {
platformView.dispose();
} catch (RuntimeException exception) {
Log.e(TAG, "Disposing platform view threw an exception", exception);
}
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController vdController = vdControllers.get(viewId);
final View embeddedView = vdController.getView();
if (embeddedView != null) {
contextToEmbeddedView.remove(embeddedView.getContext());
}
vdController.dispose();
vdControllers.remove(viewId);
return;
}
// The platform view is displayed using a TextureLayer and is inserted in the view
// hierarchy.
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (viewWrapper != null) {
viewWrapper.removeAllViews();
viewWrapper.release();
viewWrapper.unsetOnDescendantFocusChangeListener();
final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
if (wrapperParent != null) {
wrapperParent.removeView(viewWrapper);
}
viewWrappers.remove(viewId);
return;
}
// The platform view is displayed using a PlatformViewLayer.
final FlutterMutatorView parentView = platformViewParent.get(viewId);
if (parentView != null) {
parentView.removeAllViews();
parentView.unsetOnDescendantFocusChangeListener();
final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
if (mutatorViewParent != null) {
mutatorViewParent.removeView(parentView);
}
platformViewParent.remove(viewId);
}
}
@Override
public void offset(int viewId, double top, double left) {
if (usesVirtualDisplay(viewId)) {
// Virtual displays don't need an accessibility offset.
return;
}
// For platform views that use TextureView and are in the view hierarchy, set
// an offset to the wrapper view.
// This ensures that the accessibility highlights are drawn in the expected position on
// screen.
// This offset doesn't affect the position of the embeded view by itself since the GL
// texture is positioned by the Flutter engine, which knows where to position different
// types of layers.
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (viewWrapper == null) {
Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId);
return;
}
final int physicalTop = toPhysicalPixels(top);
final int physicalLeft = toPhysicalPixels(left);
final FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) viewWrapper.getLayoutParams();
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
viewWrapper.setLayoutParams(layoutParams);
}
@Override
public void resize(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
@NonNull PlatformViewsChannel.PlatformViewBufferResized onComplete) {
final int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
final int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
final int viewId = request.viewId;
if (usesVirtualDisplay(viewId)) {
final float originalDisplayDensity = getDisplayDensity();
final VirtualDisplayController vdController = vdControllers.get(viewId);
// Resizing involved moving the platform view to a new virtual display. Doing so
// potentially results in losing an active input connection. To make sure we preserve
// the input connection when resizing we lock it here and unlock after the resize is
// complete.
lockInputConnection(vdController);
vdController.resize(
physicalWidth,
physicalHeight,
() -> {
unlockInputConnection(vdController);
// Converting back to logic pixels requires a context, which may no longer be
// available. If that happens, assume the same logic/physical relationship as
// was present when the request arrived.
final float displayDensity =
context == null ? originalDisplayDensity : getDisplayDensity();
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(vdController.getRenderTargetWidth(), displayDensity),
toLogicalPixels(vdController.getRenderTargetHeight(), displayDensity)));
});
return;
}
final PlatformView platformView = platformViews.get(viewId);
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (platformView == null || viewWrapper == null) {
Log.e(TAG, "Resizing unknown platform view with id: " + viewId);
return;
}
// Resize the buffer only when the current buffer size is smaller than the new size.
// This is required to prevent a situation when smooth keyboard animation
// resizes the texture too often, such that the GPU and the platform thread don't agree on
// the timing of the new size.
// Resizing the texture causes pixel stretching since the size of the GL texture used in
// the engine is set by the framework, but the texture buffer size is set by the
// platform down below.
if (physicalWidth > viewWrapper.getRenderTargetWidth()
|| physicalHeight > viewWrapper.getRenderTargetHeight()) {
viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
}
final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams();
viewWrapperLayoutParams.width = physicalWidth;
viewWrapperLayoutParams.height = physicalHeight;
viewWrapper.setLayoutParams(viewWrapperLayoutParams);
final View embeddedView = platformView.getView();
if (embeddedView != null) {
final ViewGroup.LayoutParams embeddedViewLayoutParams = embeddedView.getLayoutParams();
embeddedViewLayoutParams.width = physicalWidth;
embeddedViewLayoutParams.height = physicalHeight;
embeddedView.setLayoutParams(embeddedViewLayoutParams);
}
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(viewWrapper.getRenderTargetWidth()),
toLogicalPixels(viewWrapper.getRenderTargetHeight())));
}
@Override
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
final int viewId = touch.viewId;
final float density = context.getResources().getDisplayMetrics().density;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController vdController = vdControllers.get(viewId);
final MotionEvent event = toMotionEvent(density, touch, true);
vdController.dispatchTouchEvent(event);
return;
}
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
return;
}
final View view = platformView.getView();
if (view == null) {
Log.e(TAG, "Sending touch to a null view with id: " + viewId);
return;
}
final MotionEvent event = toMotionEvent(density, touch, false);
view.dispatchTouchEvent(event);
}
@Override
public void setDirection(int viewId, int direction) {
if (!validateDirection(direction)) {
throw new IllegalStateException(
"Trying to set unknown direction value: "
+ direction
+ "(view id: "
+ viewId
+ ")");
}
View embeddedView;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
embeddedView = controller.getView();
} else {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
return;
}
embeddedView = platformView.getView();
}
if (embeddedView == null) {
Log.e(TAG, "Setting direction to a null view with id: " + viewId);
return;
}
embeddedView.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
View embeddedView;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
embeddedView = controller.getView();
} else {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
return;
}
embeddedView = platformView.getView();
}
if (embeddedView == null) {
Log.e(TAG, "Clearing focus on a null view with id: " + viewId);
return;
}
embeddedView.clearFocus();
}
@Override
public void synchronizeToNativeViewHierarchy(boolean yes) {
synchronizeToNativeViewHierarchy = yes;
}
};
/// Throws an exception if the SDK version is below minSdkVersion.
private void enforceMinimumAndroidApiVersion(int minSdkVersion) {
if (Build.VERSION.SDK_INT < minSdkVersion) {
throw new IllegalStateException(
"Trying to use platform views with API "
+ Build.VERSION.SDK_INT
+ ", required API level is: "
+ minSdkVersion);
}
}
private void ensureValidRequest(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
+ request.viewId
+ ")");
}
}
// Creates a platform view based on `request`, performs configuration that's common to
// all display modes, and adds it to `platformViews`.
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public PlatformView createPlatformView(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request, boolean wrapContext) {
final PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
if (viewFactory == null) {
throw new IllegalStateException(
"Trying to create a platform view of unregistered type: " + request.viewType);
}
Object createParams = null;
if (request.params != null) {
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
// In some display modes, the context needs to be modified during display.
// TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see
// https://github.com/flutter/flutter/issues/113449
final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context;
final PlatformView platformView =
viewFactory.create(mutableContext, request.viewId, createParams);
// Configure the view to match the requested layout direction.
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
embeddedView.setLayoutDirection(request.direction);
platformViews.put(request.viewId, platformView);
maybeInvokeOnFlutterViewAttached(platformView);
return platformView;
}
// Configures the view for Hybrid Composition mode.
private void configureForHybridComposition(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
enforceMinimumAndroidApiVersion(19);
Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId);
}
// Configures the view for Virtual Display mode, returning the associated texture ID.
private long configureForVirtualDisplay(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// This mode adds the view to a virtual display, which is wired up to a GL texture that
// is composed by the Flutter engine.
// API level 20 is required to use VirtualDisplay#setSurface.
enforceMinimumAndroidApiVersion(20);
Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId);
final PlatformViewRenderTarget renderTarget = makePlatformViewRenderTarget(textureRegistry);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
final VirtualDisplayController vdController =
VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
platformView,
renderTarget,
physicalWidth,
physicalHeight,
request.viewId,
null,
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
}
});
if (vdController == null) {
throw new IllegalStateException(
"Failed creating virtual display for a "
+ request.viewType
+ " with id: "
+ request.viewId);
}
// The embedded view doesn't need to be sized in Virtual Display mode because the
// virtual display itself is sized.
vdControllers.put(request.viewId, vdController);
final View embeddedView = platformView.getView();
contextToEmbeddedView.put(embeddedView.getContext(), embeddedView);
return renderTarget.getId();
}
// Configures the view for Texture Layer Hybrid Composition mode, returning the associated
// texture ID.
@TargetApi(API_LEVELS.API_23)
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public long configureForTextureLayerComposition(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// This mode attaches the view to the Android view hierarchy and record its drawing
// operations, so they can be forwarded to a GL texture that is composed by the
// Flutter engine.
// API level 23 is required to use Surface#lockHardwareCanvas().
enforceMinimumAndroidApiVersion(23);
Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
PlatformViewWrapper viewWrapper;
long textureId;
if (usesSoftwareRendering) {
viewWrapper = new PlatformViewWrapper(context);
textureId = -1;
} else {
final PlatformViewRenderTarget renderTarget = makePlatformViewRenderTarget(textureRegistry);
viewWrapper = new PlatformViewWrapper(context, renderTarget);
textureId = renderTarget.getId();
}
viewWrapper.setTouchProcessor(androidTouchProcessor);
viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
final FrameLayout.LayoutParams viewWrapperLayoutParams =
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
// Size and position the view wrapper.
final int physicalTop = toPhysicalPixels(request.logicalTop);
final int physicalLeft = toPhysicalPixels(request.logicalLeft);
viewWrapperLayoutParams.topMargin = physicalTop;
viewWrapperLayoutParams.leftMargin = physicalLeft;
viewWrapper.setLayoutParams(viewWrapperLayoutParams);
// Size the embedded view.
final View embeddedView = platformView.getView();
embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));
// Accessibility in the embedded view is initially disabled because if a Flutter app
// disabled accessibility in the first frame, the embedding won't receive an update to
// disable accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas
// when the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has
// been created.
embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
// Add the embedded view to the wrapper.
viewWrapper.addView(embeddedView);
// Listen for focus changed in any subview, so the framework is notified when the platform
// view is focused.
viewWrapper.setOnDescendantFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
} else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(request.viewId);
}
});
flutterView.addView(viewWrapper);
viewWrappers.append(request.viewId, viewWrapper);
maybeInvokeOnFlutterViewAttached(platformView);
return textureId;
}
@VisibleForTesting
public MotionEvent toMotionEvent(
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
if (!usingVirtualDiplay && trackedEvent != null) {
// We have the original event, deliver it as it will pass the verifiable
// input check.
return trackedEvent;
}
// We are in virtual display mode or don't have a reference to the original MotionEvent.
// In this case we manually recreate a MotionEvent to be delivered. This MotionEvent
// will fail the verifiable input check.
// Pointer coordinates in the tracked events are global to FlutterView
// framework converts them to be local to a widget, given that
// motion events operate on local coords, we need to replace these in the tracked
// event with their local counterparts.
PointerProperties[] pointerProperties =
parsePointerPropertiesList(touch.rawPointerPropertiesList)
.toArray(new PointerProperties[touch.pointerCount]);
PointerCoords[] pointerCoords =
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
// TODO (kaushikiska) : warn that we are potentially using an untracked
// event in the platform views.
return MotionEvent.obtain(
touch.downTime.longValue(),
touch.eventTime.longValue(),
touch.action,
touch.pointerCount,
pointerProperties,
pointerCoords,
touch.metaState,
touch.buttonState,
touch.xPrecision,
touch.yPrecision,
touch.deviceId,
touch.edgeFlags,
touch.source,
touch.flags);
}
public PlatformViewsController() {
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
contextToEmbeddedView = new HashMap<>();
overlayLayerViews = new SparseArray<>();
currentFrameUsedOverlayLayerIds = new HashSet<>();
currentFrameUsedPlatformViewIds = new HashSet<>();
viewWrappers = new SparseArray<>();
platformViews = new SparseArray<>();
platformViewParent = new SparseArray<>();
motionEventTracker = MotionEventTracker.getInstance();
}
/**
* Attaches this platform views controller to its input and output channels.
*
* @param context The base context that will be passed to embedded views created by this
* controller. This should be the context of the Activity hosting the Flutter application.
* @param textureRegistry The texture registry which provides the output textures into which the
* embedded views will be rendered.
* @param dartExecutor The dart execution context, which is used to set up a system channel.
*/
public void attach(
@Nullable Context context,
@NonNull TextureRegistry textureRegistry,
@NonNull DartExecutor dartExecutor) {
if (this.context != null) {
throw new AssertionError(
"A PlatformViewsController can only be attached to a single output target.\n"
+ "attach was called while the PlatformViewsController was already attached.");
}
this.context = context;
this.textureRegistry = textureRegistry;
platformViewsChannel = new PlatformViewsChannel(dartExecutor);
platformViewsChannel.setPlatformViewsHandler(channelHandler);
}
/**
* Sets whether Flutter uses software rendering.
*
* <p>When software rendering is used, no GL context is available on the raster thread. When this
* is set to true, there's no Flutter composition of Android views and Flutter widgets since GL
* textures cannot be used.
*
* <p>Software rendering is only used for testing in emulators, and it should never be set to true
* in a production environment.
*
* @param useSoftwareRendering Whether software rendering is used.
*/
public void setSoftwareRendering(boolean useSoftwareRendering) {
usesSoftwareRendering = useSoftwareRendering;
}
/**
* Detaches this platform views controller.
*
* <p>This is typically called when a Flutter applications moves to run in the background, or is
* destroyed. After calling this the platform views controller will no longer listen to it's
* previous messenger, and will not maintain references to the texture registry, context, and
* messenger passed to the previous attach call.
*/
@UiThread
public void detach() {
if (platformViewsChannel != null) {
platformViewsChannel.setPlatformViewsHandler(null);
}
destroyOverlaySurfaces();
platformViewsChannel = null;
context = null;
textureRegistry = null;
}
/**
* Attaches the controller to a {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* after the device rotates since the FlutterView is recreated after a rotation.
*/
public void attachToView(@NonNull FlutterView newFlutterView) {
flutterView = newFlutterView;
// Add wrapper for platform views that use GL texture.
for (int index = 0; index < viewWrappers.size(); index++) {
final PlatformViewWrapper view = viewWrappers.valueAt(index);
flutterView.addView(view);
}
// Add wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.addView(view);
}
// Notify platform views that they are now attached to a FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewAttached(flutterView);
}
}
/**
* Detaches the controller from {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* when the device rotates since the FlutterView is detached from the fragment. The next time the
* fragment needs to be displayed, a new Flutter view is created, so attachToView is called again.
*/
public void detachFromView() {
// Remove wrapper for platform views that use GL texture.
for (int index = 0; index < viewWrappers.size(); index++) {
final PlatformViewWrapper view = viewWrappers.valueAt(index);
flutterView.removeView(view);
}
// Remove wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.removeView(view);
}
destroyOverlaySurfaces();
removeOverlaySurfaces();
flutterView = null;
flutterViewConvertedToImageView = false;
// Notify that the platform view have been detached from FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewDetached();
}
}
private void maybeInvokeOnFlutterViewAttached(PlatformView view) {
if (flutterView == null) {
Log.i(TAG, "null flutterView");
// There is currently no FlutterView that we are attached to.
return;
}
view.onFlutterViewAttached(flutterView);
}
@Override
public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) {
accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
}
@Override
public void detachAccessibilityBridge() {
accessibilityEventsDelegate.setAccessibilityBridge(null);
}
/**
* Attaches this controller to a text input plugin.
*
* <p>While a text input plugin is available, the platform views controller interacts with it to
* facilitate delegation of text input connections to platform views.
*
* <p>A platform views controller should be attached to a text input plugin whenever it is
* possible for the Flutter framework to receive text input.
*/
public void attachTextInputPlugin(@NonNull TextInputPlugin textInputPlugin) {
this.textInputPlugin = textInputPlugin;
}
/** Detaches this controller from the currently attached text input plugin. */
public void detachTextInputPlugin() {
textInputPlugin = null;
}
/**
* Returns true if Flutter should perform input connection proxying for the view.
*
* <p>If the view is a platform view managed by this platform views controller returns true. Else
* if the view was created in a platform view's VD, delegates the decision to the platform view's
* {@link View#checkInputConnectionProxy(View)} method. Else returns false.
*/
public boolean checkInputConnectionProxy(@Nullable View view) {
// View can be null on some devices
// See: https://github.com/flutter/flutter/issues/36517
if (view == null) {
return false;
}
if (!contextToEmbeddedView.containsKey(view.getContext())) {
return false;
}
View platformView = contextToEmbeddedView.get(view.getContext());
if (platformView == view) {
return true;
}
return platformView.checkInputConnectionProxy(view);
}
public PlatformViewRegistry getRegistry() {
return registry;
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} attaches to JNI.
*/
public void onAttachedToJNI() {
// Currently no action needs to be taken after JNI attachment.
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} detaches from JNI.
*/
public void onDetachedFromJNI() {
diposeAllViews();
}
public void onPreEngineRestart() {
diposeAllViews();
}
@Override
@Nullable
public View getPlatformViewById(int viewId) {
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
return controller.getView();
}
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
return null;
}
return platformView.getView();
}
@Override
public boolean usesVirtualDisplay(int id) {
return vdControllers.containsKey(id);
}
private void lockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.lockPlatformViewInputConnection();
controller.onInputConnectionLocked();
}
private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.unlockPlatformViewInputConnection();
controller.onInputConnectionUnlocked();
}
private static PlatformViewRenderTarget makePlatformViewRenderTarget(
TextureRegistry textureRegistry) {
if (enableSurfaceProducerRenderTarget && Build.VERSION.SDK_INT >= API_LEVELS.API_29) {
final TextureRegistry.SurfaceProducer textureEntry = textureRegistry.createSurfaceProducer();
Log.i(TAG, "PlatformView is using SurfaceProducer backend");
return new SurfaceProducerPlatformViewRenderTarget(textureEntry);
}
if (enableImageRenderTarget && Build.VERSION.SDK_INT >= API_LEVELS.API_29) {
final TextureRegistry.ImageTextureEntry textureEntry = textureRegistry.createImageTexture();
Log.i(TAG, "PlatformView is using ImageReader backend");
return new ImageReaderPlatformViewRenderTarget(textureEntry);
}
final TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
Log.i(TAG, "PlatformView is using SurfaceTexture backend");
return new SurfaceTexturePlatformViewRenderTarget(textureEntry);
}
private static boolean validateDirection(int direction) {
return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL;
}
@SuppressWarnings("unchecked")
private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) {
List<Object> rawProperties = (List<Object>) rawPropertiesList;
List<PointerProperties> pointerProperties = new ArrayList<>();
for (Object o : rawProperties) {
pointerProperties.add(parsePointerProperties(o));
}
return pointerProperties;
}
@SuppressWarnings("unchecked")
private static PointerProperties parsePointerProperties(Object rawProperties) {
List<Object> propertiesList = (List<Object>) rawProperties;
PointerProperties properties = new MotionEvent.PointerProperties();