Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
FlutterView.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.embedding.android;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.annotation.SuppressLint;
10import android.annotation.TargetApi;
11import android.app.Activity;
12import android.content.Context;
13import android.content.res.Configuration;
14import android.database.ContentObserver;
15import android.graphics.Insets;
16import android.graphics.Rect;
17import android.os.Build;
18import android.os.Handler;
19import android.os.Looper;
20import android.provider.Settings;
21import android.text.format.DateFormat;
22import android.util.AttributeSet;
23import android.util.SparseArray;
24import android.view.DisplayCutout;
25import android.view.KeyEvent;
26import android.view.MotionEvent;
27import android.view.PointerIcon;
28import android.view.Surface;
29import android.view.View;
30import android.view.ViewConfiguration;
31import android.view.ViewGroup;
32import android.view.ViewStructure;
33import android.view.WindowInsets;
34import android.view.WindowManager;
35import android.view.accessibility.AccessibilityManager;
36import android.view.accessibility.AccessibilityNodeProvider;
37import android.view.autofill.AutofillValue;
38import android.view.inputmethod.EditorInfo;
39import android.view.inputmethod.InputConnection;
40import android.view.textservice.SpellCheckerInfo;
41import android.view.textservice.TextServicesManager;
42import android.widget.FrameLayout;
43import androidx.annotation.NonNull;
44import androidx.annotation.Nullable;
45import androidx.annotation.RequiresApi;
46import androidx.annotation.VisibleForTesting;
47import androidx.core.content.ContextCompat;
48import androidx.core.util.Consumer;
49import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
50import androidx.window.layout.DisplayFeature;
51import androidx.window.layout.FoldingFeature;
52import androidx.window.layout.FoldingFeature.OcclusionType;
53import androidx.window.layout.FoldingFeature.State;
54import androidx.window.layout.WindowInfoTracker;
55import androidx.window.layout.WindowLayoutInfo;
56import io.flutter.Log;
57import io.flutter.embedding.engine.FlutterEngine;
58import io.flutter.embedding.engine.renderer.FlutterRenderer;
59import io.flutter.embedding.engine.renderer.FlutterRenderer.DisplayFeatureState;
60import io.flutter.embedding.engine.renderer.FlutterRenderer.DisplayFeatureType;
61import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
62import io.flutter.embedding.engine.renderer.RenderSurface;
63import io.flutter.embedding.engine.systemchannels.SettingsChannel;
64import io.flutter.plugin.common.BinaryMessenger;
65import io.flutter.plugin.editing.SpellCheckPlugin;
66import io.flutter.plugin.editing.TextInputPlugin;
67import io.flutter.plugin.localization.LocalizationPlugin;
68import io.flutter.plugin.mouse.MouseCursorPlugin;
69import io.flutter.plugin.platform.PlatformViewsController;
70import io.flutter.util.ViewUtils;
71import io.flutter.view.AccessibilityBridge;
72import java.lang.reflect.InvocationTargetException;
73import java.lang.reflect.Method;
74import java.util.ArrayList;
75import java.util.HashSet;
76import java.util.List;
77import java.util.Set;
78
79/**
80 * Displays a Flutter UI on an Android device.
81 *
82 * <p>A {@code FlutterView}'s UI is painted by a corresponding {@link
83 * io.flutter.embedding.engine.FlutterEngine}.
84 *
85 * <p>A {@code FlutterView} can operate in 2 different {@link
86 * io.flutter.embedding.android.RenderMode}s:
87 *
88 * <ol>
89 * <li>{@link io.flutter.embedding.android.RenderMode#surface}, which paints a Flutter UI to a
90 * {@link android.view.SurfaceView}. This mode has the best performance, but a {@code
91 * FlutterView} in this mode cannot be positioned between 2 other Android {@code View}s in the
92 * z-index, nor can it be animated/transformed. Unless the special capabilities of a {@link
93 * android.graphics.SurfaceTexture} are required, developers should strongly prefer this
94 * render mode.
95 * <li>{@link io.flutter.embedding.android.RenderMode#texture}, which paints a Flutter UI to a
96 * {@link android.graphics.SurfaceTexture}. This mode is not as performant as {@link
97 * io.flutter.embedding.android.RenderMode#surface}, but a {@code FlutterView} in this mode
98 * can be animated and transformed, as well as positioned in the z-index between 2+ other
99 * Android {@code Views}. Unless the special capabilities of a {@link
100 * android.graphics.SurfaceTexture} are required, developers should strongly prefer the {@link
101 * io.flutter.embedding.android.RenderMode#surface} render mode.
102 * </ol>
103 *
104 * See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more
105 * information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}.
106 */
107public class FlutterView extends FrameLayout
108 implements MouseCursorPlugin.MouseCursorViewDelegate, KeyboardManager.ViewDelegate {
109 private static final String TAG = "FlutterView";
110
111 // Internal view hierarchy references.
112 @Nullable private FlutterSurfaceView flutterSurfaceView;
113 @Nullable private FlutterTextureView flutterTextureView;
114 @Nullable private FlutterImageView flutterImageView;
115 @Nullable @VisibleForTesting /* package */ RenderSurface renderSurface;
116 @Nullable private RenderSurface previousRenderSurface;
117 private final Set<FlutterUiDisplayListener> flutterUiDisplayListeners = new HashSet<>();
118 private boolean isFlutterUiDisplayed;
119
120 // Connections to a Flutter execution context.
121 @Nullable private FlutterEngine flutterEngine;
122
123 @NonNull
124 private final Set<FlutterEngineAttachmentListener> flutterEngineAttachmentListeners =
125 new HashSet<>();
126
127 // Components that process various types of Android View input and events,
128 // possibly storing intermediate state, and communicating those events to Flutter.
129 //
130 // These components essentially add some additional behavioral logic on top of
131 // existing, stateless system channels, e.g., MouseCursorChannel, TextInputChannel, etc.
132 @Nullable private MouseCursorPlugin mouseCursorPlugin;
133 @Nullable private TextInputPlugin textInputPlugin;
134 @Nullable private SpellCheckPlugin spellCheckPlugin;
135 @Nullable private LocalizationPlugin localizationPlugin;
136 @Nullable private KeyboardManager keyboardManager;
137 @Nullable private AndroidTouchProcessor androidTouchProcessor;
138 @Nullable private AccessibilityBridge accessibilityBridge;
139 @Nullable private TextServicesManager textServicesManager;
140
141 // Provides access to foldable/hinge information
142 @Nullable private WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo;
143 // Directly implemented View behavior that communicates with Flutter.
144 private final FlutterRenderer.ViewportMetrics viewportMetrics =
145 new FlutterRenderer.ViewportMetrics();
146
147 private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener =
148 new AccessibilityBridge.OnAccessibilityChangeListener() {
149 @Override
150 public void onAccessibilityChanged(
151 boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
152 resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
153 }
154 };
155
156 private final ContentObserver systemSettingsObserver =
157 new ContentObserver(new Handler(Looper.getMainLooper())) {
158 @Override
159 public void onChange(boolean selfChange) {
160 super.onChange(selfChange);
161 if (flutterEngine == null) {
162 return;
163 }
164 Log.v(TAG, "System settings changed. Sending user settings to Flutter.");
165 sendUserSettingsToFlutter();
166 }
167
168 @Override
169 public boolean deliverSelfNotifications() {
170 // The Flutter app may change system settings.
171 return true;
172 }
173 };
174
175 private final FlutterUiDisplayListener flutterUiDisplayListener =
176 new FlutterUiDisplayListener() {
177 @Override
178 public void onFlutterUiDisplayed() {
179 isFlutterUiDisplayed = true;
180
181 for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
182 listener.onFlutterUiDisplayed();
183 }
184 }
185
186 @Override
187 public void onFlutterUiNoLongerDisplayed() {
188 isFlutterUiDisplayed = false;
189
190 for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
191 listener.onFlutterUiNoLongerDisplayed();
192 }
193 }
194 };
195
196 private final Consumer<WindowLayoutInfo> windowInfoListener =
197 new Consumer<WindowLayoutInfo>() {
198 @Override
199 public void accept(WindowLayoutInfo layoutInfo) {
201 }
202 };
203
204 /**
205 * Constructs a {@code FlutterView} programmatically, without any XML attributes.
206 *
207 * <p>
208 *
209 * <ul>
210 * <li>A {@link FlutterSurfaceView} is used to render the Flutter UI.
211 * <li>{@code transparencyMode} defaults to {@link TransparencyMode#opaque}.
212 * </ul>
213 *
214 * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
215 * compatible with {@link PlatformViewsController}.
216 */
217 public FlutterView(@NonNull Context context) {
218 this(context, null, new FlutterSurfaceView(context));
219 }
220
221 /**
222 * Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
223 * #FlutterView(Context, FlutterTextureView)} or {@link #FlutterView(Context, FlutterImageView)}
224 * instead.
225 */
226 @Deprecated
227 public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) {
228 super(context, null);
229
230 if (renderMode == RenderMode.surface) {
231 flutterSurfaceView = new FlutterSurfaceView(context);
232 renderSurface = flutterSurfaceView;
233 } else if (renderMode == RenderMode.texture) {
234 flutterTextureView = new FlutterTextureView(context);
235 renderSurface = flutterTextureView;
236 } else {
237 throw new IllegalArgumentException(
238 "RenderMode not supported with this constructor: " + renderMode);
239 }
240
241 init();
242 }
243
244 /**
245 * Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
246 * #FlutterView(Context, FlutterTextureView)} instead, and configure the incoming {@code
247 * FlutterSurfaceView} or {@code FlutterTextureView} for transparency as desired.
248 *
249 * <p>Constructs a {@code FlutterView} programmatically, without any XML attributes, uses a {@link
250 * FlutterSurfaceView} to render the Flutter UI, and allows selection of a {@code
251 * transparencyMode}.
252 *
253 * <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
254 * compatible with {@link PlatformViewsController}.
255 */
256 @Deprecated
257 public FlutterView(@NonNull Context context, @NonNull TransparencyMode transparencyMode) {
258 this(
259 context,
260 null,
261 new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent));
262 }
263
264 /**
265 * Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given
266 * {@link FlutterSurfaceView} to render the Flutter UI, and allows selection of a {@code
267 * transparencyMode}.
268 *
269 * <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
270 * compatible with {@link PlatformViewsController}.
271 */
272 public FlutterView(@NonNull Context context, @NonNull FlutterSurfaceView flutterSurfaceView) {
273 this(context, null, flutterSurfaceView);
274 }
275
276 /**
277 * Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given
278 * {@link FlutterTextureView} to render the Flutter UI, and allows selection of a {@code
279 * transparencyMode}.
280 *
281 * <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
282 * compatible with {@link PlatformViewsController}.
283 */
284 public FlutterView(@NonNull Context context, @NonNull FlutterTextureView flutterTextureView) {
285 this(context, null, flutterTextureView);
286 }
287
288 /**
289 * Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given
290 * {@link FlutterImageView} to render the Flutter UI.
291 *
292 * <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
293 * compatible with {@link PlatformViewsController}.
294 */
295 public FlutterView(@NonNull Context context, @NonNull FlutterImageView flutterImageView) {
296 this(context, null, flutterImageView);
297 }
298
299 /**
300 * Constructs a {@code FlutterView} in an XML-inflation-compliant manner.
301 *
302 * <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
303 * compatible with {@link PlatformViewsController}.
304 */
305 // TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
306 public FlutterView(@NonNull Context context, @Nullable AttributeSet attrs) {
307 this(context, attrs, new FlutterSurfaceView(context));
308 }
309
310 /**
311 * Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
312 * #FlutterView(Context, FlutterTextureView)} instead, and configure the incoming {@code
313 * FlutterSurfaceView} or {@code FlutterTextureView} for transparency as desired.
314 */
315 @Deprecated
317 @NonNull Context context,
318 @NonNull RenderMode renderMode,
319 @NonNull TransparencyMode transparencyMode) {
320 super(context, null);
321
322 if (renderMode == RenderMode.surface) {
323 flutterSurfaceView =
324 new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent);
325 renderSurface = flutterSurfaceView;
326 } else if (renderMode == RenderMode.texture) {
327 flutterTextureView = new FlutterTextureView(context);
328 renderSurface = flutterTextureView;
329 } else {
330 throw new IllegalArgumentException(
331 "RenderMode not supported with this constructor: " + renderMode);
332 }
333
334 init();
335 }
336
337 private FlutterView(
338 @NonNull Context context,
339 @Nullable AttributeSet attrs,
340 @NonNull FlutterSurfaceView flutterSurfaceView) {
341 super(context, attrs);
342
343 this.flutterSurfaceView = flutterSurfaceView;
344 this.renderSurface = flutterSurfaceView;
345
346 init();
347 }
348
349 private FlutterView(
350 @NonNull Context context,
351 @Nullable AttributeSet attrs,
352 @NonNull FlutterTextureView flutterTextureView) {
353 super(context, attrs);
354
355 this.flutterTextureView = flutterTextureView;
356 this.renderSurface = flutterTextureView;
357
358 init();
359 }
360
361 private FlutterView(
362 @NonNull Context context,
363 @Nullable AttributeSet attrs,
364 @NonNull FlutterImageView flutterImageView) {
365 super(context, attrs);
366
367 this.flutterImageView = flutterImageView;
368 this.renderSurface = flutterImageView;
369
370 init();
371 }
372
373 private void init() {
374 Log.v(TAG, "Initializing FlutterView");
375
376 if (flutterSurfaceView != null) {
377 Log.v(TAG, "Internally using a FlutterSurfaceView.");
378 addView(flutterSurfaceView);
379 } else if (flutterTextureView != null) {
380 Log.v(TAG, "Internally using a FlutterTextureView.");
381 addView(flutterTextureView);
382 } else {
383 Log.v(TAG, "Internally using a FlutterImageView.");
384 addView(flutterImageView);
385 }
386
387 // FlutterView needs to be focusable so that the InputMethodManager can interact with it.
388 setFocusable(true);
389 setFocusableInTouchMode(true);
390 if (Build.VERSION.SDK_INT >= API_LEVELS.API_26) {
391 setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);
392 }
393 }
394
395 /**
396 * Returns true if an attached {@link io.flutter.embedding.engine.FlutterEngine} has rendered at
397 * least 1 frame to this {@code FlutterView}.
398 *
399 * <p>Returns false if no {@link io.flutter.embedding.engine.FlutterEngine} is attached.
400 *
401 * <p>This flag is specific to a given {@link io.flutter.embedding.engine.FlutterEngine}. The
402 * following hypothetical timeline demonstrates how this flag changes over time.
403 *
404 * <ol>
405 * <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false
406 * <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true
407 * <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false
408 * <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false
409 * <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true
410 * </ol>
411 */
412 public boolean hasRenderedFirstFrame() {
413 return isFlutterUiDisplayed;
414 }
415
416 /**
417 * Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
418 * first rendered frame.
419 */
420 public void addOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) {
421 flutterUiDisplayListeners.add(listener);
422 }
423
424 /**
425 * Removes the given {@code listener}, which was previously added with {@link
426 * #addOnFirstFrameRenderedListener(FlutterUiDisplayListener)}.
427 */
428 public void removeOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) {
429 flutterUiDisplayListeners.remove(listener);
430 }
431
432 // ------- Start: Process View configuration that Flutter cares about. ------
433 /**
434 * Sends relevant configuration data from Android to Flutter when the Android {@link
435 * Configuration} changes.
436 *
437 * <p>The Android {@link Configuration} might change as a result of device orientation change,
438 * device language change, device text scale factor change, etc.
439 */
440 @Override
441 protected void onConfigurationChanged(@NonNull Configuration newConfig) {
442 super.onConfigurationChanged(newConfig);
443 // We've observed on Android Q that going to the background, changing
444 // orientation, and bringing the app back to foreground results in a sequence
445 // of detatch from flutterEngine, onConfigurationChanged, followed by attach
446 // to flutterEngine.
447 // No-op here so that we avoid NPE; these channels will get notified once
448 // the activity or fragment tell the view to attach to the Flutter engine
449 // again (e.g. in onStart).
450 if (flutterEngine != null) {
451 Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter.");
452 localizationPlugin.sendLocalesToFlutter(newConfig);
453 sendUserSettingsToFlutter();
454
455 ViewUtils.calculateMaximumDisplayMetrics(getContext(), flutterEngine);
456 }
457 }
458
459 /**
460 * Invoked when this {@code FlutterView} changes size, including upon initial measure.
461 *
462 * <p>The initial measure reports an {@code oldWidth} and {@code oldHeight} of zero.
463 *
464 * <p>Flutter cares about the width and height of the view that displays it on the host platform.
465 * Therefore, when this method is invoked, the new width and height are communicated to Flutter as
466 * the "physical size" of the view that displays Flutter's UI.
467 */
468 @Override
469 protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
470 super.onSizeChanged(width, height, oldWidth, oldHeight);
471 Log.v(
472 TAG,
473 "Size changed. Sending Flutter new viewport metrics. FlutterView was "
474 + oldWidth
475 + " x "
476 + oldHeight
477 + ", it is now "
478 + width
479 + " x "
480 + height);
481 viewportMetrics.width = width;
482 viewportMetrics.height = height;
483 sendViewportMetricsToFlutter();
484 }
485
486 @VisibleForTesting()
488 try {
490 new WindowInfoTrackerCallbackAdapter(
491 WindowInfoTracker.Companion.getOrCreate(getContext())));
492 } catch (NoClassDefFoundError noClassDefFoundError) {
493 // Testing environment uses gn/javac, which does not work with aar files. This is why aar
494 // are converted to jar files, losing resources and other android-specific files.
495 // androidx.window does contain resources, which causes it to fail during testing, since the
496 // class androidx.window.R is not found.
497 // This method is mocked in the tests involving androidx.window, but this catch block is
498 // needed for other tests, which would otherwise fail during onAttachedToWindow().
499 return null;
500 }
501 }
502
503 /**
504 * Invoked when this is attached to the window.
505 *
506 * <p>We register for {@link androidx.window.layout.WindowInfoTracker} updates.
507 */
508 @Override
509 protected void onAttachedToWindow() {
510 super.onAttachedToWindow();
511 this.windowInfoRepo = createWindowInfoRepo();
512 Activity activity = ViewUtils.getActivity(getContext());
513 if (windowInfoRepo != null && activity != null) {
514 windowInfoRepo.addWindowLayoutInfoListener(
515 activity, ContextCompat.getMainExecutor(getContext()), windowInfoListener);
516 }
517 }
518
519 /**
520 * Invoked when this is detached from the window.
521 *
522 * <p>We unregister from {@link androidx.window.layout.WindowInfoTracker} updates.
523 */
524 @Override
525 protected void onDetachedFromWindow() {
526 if (windowInfoRepo != null) {
527 windowInfoRepo.removeWindowLayoutInfoListener(windowInfoListener);
528 }
529 this.windowInfoRepo = null;
530 super.onDetachedFromWindow();
531 }
532
533 /**
534 * Refresh {@link androidx.window.layout.WindowInfoTracker} and {@link android.view.DisplayCutout}
535 * display features. Fold, hinge and cutout areas are populated here.
536 */
537 @TargetApi(API_LEVELS.API_28)
538 protected void setWindowInfoListenerDisplayFeatures(WindowLayoutInfo layoutInfo) {
539 List<DisplayFeature> displayFeatures = layoutInfo.getDisplayFeatures();
540 List<FlutterRenderer.DisplayFeature> result = new ArrayList<>();
541
542 // Data from WindowInfoTracker display features. Fold and hinge areas are
543 // populated here.
544 for (DisplayFeature displayFeature : displayFeatures) {
545 Log.v(
546 TAG,
547 "WindowInfoTracker Display Feature reported with bounds = "
548 + displayFeature.getBounds().toString()
549 + " and type = "
550 + displayFeature.getClass().getSimpleName());
551 if (displayFeature instanceof FoldingFeature) {
552 DisplayFeatureType type;
553 DisplayFeatureState state;
554 final FoldingFeature feature = (FoldingFeature) displayFeature;
555 if (feature.getOcclusionType() == OcclusionType.FULL) {
556 type = DisplayFeatureType.HINGE;
557 } else {
558 type = DisplayFeatureType.FOLD;
559 }
560 if (feature.getState() == State.FLAT) {
561 state = DisplayFeatureState.POSTURE_FLAT;
562 } else if (feature.getState() == State.HALF_OPENED) {
563 state = DisplayFeatureState.POSTURE_HALF_OPENED;
564 } else {
565 state = DisplayFeatureState.UNKNOWN;
566 }
567 result.add(new FlutterRenderer.DisplayFeature(displayFeature.getBounds(), type, state));
568 } else {
569 result.add(
570 new FlutterRenderer.DisplayFeature(
571 displayFeature.getBounds(),
572 DisplayFeatureType.UNKNOWN,
573 DisplayFeatureState.UNKNOWN));
574 }
575 }
576
577 // Data from the DisplayCutout bounds. Cutouts for cameras and other sensors are
578 // populated here. DisplayCutout was introduced in API 28.
579 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
580 WindowInsets insets = getRootWindowInsets();
581 if (insets != null) {
582 DisplayCutout cutout = insets.getDisplayCutout();
583 if (cutout != null) {
584 for (Rect bounds : cutout.getBoundingRects()) {
585 Log.v(TAG, "DisplayCutout area reported with bounds = " + bounds.toString());
586 result.add(new FlutterRenderer.DisplayFeature(bounds, DisplayFeatureType.CUTOUT));
587 }
588 }
589 }
590 }
591 viewportMetrics.displayFeatures = result;
592 sendViewportMetricsToFlutter();
593 }
594
595 // TODO(garyq): Add support for notch cutout API: https://github.com/flutter/flutter/issues/56592
596 // Decide if we want to zero the padding of the sides. When in Landscape orientation,
597 // android may decide to place the software navigation bars on the side. When the nav
598 // bar is hidden, the reported insets should be removed to prevent extra useless space
599 // on the sides.
600 private enum ZeroSides {
601 NONE,
602 LEFT,
603 RIGHT,
604 BOTH
605 }
606
607 private ZeroSides calculateShouldZeroSides() {
608 // We get both orientation and rotation because rotation is all 4
609 // rotations relative to default rotation while orientation is portrait
610 // or landscape. By combining both, we can obtain a more precise measure
611 // of the rotation.
612 Context context = getContext();
613 int orientation = context.getResources().getConfiguration().orientation;
614 int rotation =
615 ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
616 .getDefaultDisplay()
617 .getRotation();
618
619 if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
620 if (rotation == Surface.ROTATION_90) {
621 return ZeroSides.RIGHT;
622 } else if (rotation == Surface.ROTATION_270) {
623 // In android API >= 23, the nav bar always appears on the "bottom" (USB) side.
624 return Build.VERSION.SDK_INT >= API_LEVELS.API_23 ? ZeroSides.LEFT : ZeroSides.RIGHT;
625 }
626 // Ambiguous orientation due to landscape left/right default. Zero both sides.
627 else if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
628 return ZeroSides.BOTH;
629 }
630 }
631 // Square orientation deprecated in API 16, we will not check for it and return false
632 // to be safe and not remove any unique padding for the devices that do use it.
633 return ZeroSides.NONE;
634 }
635
636 // TODO(garyq): The keyboard detection may interact strangely with
637 // https://github.com/flutter/flutter/issues/22061
638
639 // Uses inset heights and screen heights as a heuristic to determine if the insets should
640 // be padded. When the on-screen keyboard is detected, we want to include the full inset
641 // but when the inset is just the hidden nav bar, we want to provide a zero inset so the space
642 // can be used.
643 //
644 // This method is replaced by Android API 30 (R/11) getInsets() method which can take the
645 // android.view.WindowInsets.Type.ime() flag to find the keyboard inset.
646
647 private int guessBottomKeyboardInset(WindowInsets insets) {
648 int screenHeight = getRootView().getHeight();
649 // Magic number due to this being a heuristic. This should be replaced, but we have not
650 // found a clean way to do it yet (Sept. 2018)
651 final double keyboardHeightRatioHeuristic = 0.18;
652 if (insets.getSystemWindowInsetBottom() < screenHeight * keyboardHeightRatioHeuristic) {
653 // Is not a keyboard, so return zero as inset.
654 return 0;
655 } else {
656 // Is a keyboard, so return the full inset.
657 return insets.getSystemWindowInsetBottom();
658 }
659 }
660
661 /**
662 * Invoked when Android's desired window insets change, i.e., padding.
663 *
664 * <p>Flutter does not use a standard {@code View} hierarchy and therefore Flutter is unaware of
665 * these insets. Therefore, this method calculates the viewport metrics that Flutter should use
666 * and then sends those metrics to Flutter.
667 *
668 * <p>This callback is not present in API &lt; 20, which means lower API devices will see the
669 * wider than expected padding when the status and navigation bars are hidden.
670 */
671 @Override
672
673 // The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings
674 // caused by usage of Android Q APIs. These calls are safe because they are
675 // guarded.
676 @SuppressLint({"InlinedApi", "NewApi"})
677 @NonNull
678 public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
679 WindowInsets newInsets = super.onApplyWindowInsets(insets);
680
681 // getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
682 if (Build.VERSION.SDK_INT == API_LEVELS.API_29) {
683 Insets systemGestureInsets = insets.getSystemGestureInsets();
684 viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
685 viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
686 viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
687 viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
688 }
689
690 boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
691 boolean navigationBarVisible =
692 (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;
693
694 if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
695 int mask = 0;
696 if (navigationBarVisible) {
697 mask = mask | android.view.WindowInsets.Type.navigationBars();
698 }
699 if (statusBarVisible) {
700 mask = mask | android.view.WindowInsets.Type.statusBars();
701 }
702 Insets uiInsets = insets.getInsets(mask);
703 viewportMetrics.viewPaddingTop = uiInsets.top;
704 viewportMetrics.viewPaddingRight = uiInsets.right;
705 viewportMetrics.viewPaddingBottom = uiInsets.bottom;
706 viewportMetrics.viewPaddingLeft = uiInsets.left;
707
708 Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
709 viewportMetrics.viewInsetTop = imeInsets.top;
710 viewportMetrics.viewInsetRight = imeInsets.right;
711 viewportMetrics.viewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
712 viewportMetrics.viewInsetLeft = imeInsets.left;
713
714 Insets systemGestureInsets =
715 insets.getInsets(android.view.WindowInsets.Type.systemGestures());
716 viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
717 viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
718 viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
719 viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
720
721 // TODO(garyq): Expose the full rects of the display cutout.
722
723 // Take the max of the display cutout insets and existing padding to merge them
724 DisplayCutout cutout = insets.getDisplayCutout();
725 if (cutout != null) {
726 Insets waterfallInsets = cutout.getWaterfallInsets();
727 viewportMetrics.viewPaddingTop =
728 Math.max(
729 Math.max(viewportMetrics.viewPaddingTop, waterfallInsets.top),
730 cutout.getSafeInsetTop());
731 viewportMetrics.viewPaddingRight =
732 Math.max(
733 Math.max(viewportMetrics.viewPaddingRight, waterfallInsets.right),
734 cutout.getSafeInsetRight());
735 viewportMetrics.viewPaddingBottom =
736 Math.max(
737 Math.max(viewportMetrics.viewPaddingBottom, waterfallInsets.bottom),
738 cutout.getSafeInsetBottom());
739 viewportMetrics.viewPaddingLeft =
740 Math.max(
741 Math.max(viewportMetrics.viewPaddingLeft, waterfallInsets.left),
742 cutout.getSafeInsetLeft());
743 }
744 } else {
745 // We zero the left and/or right sides to prevent the padding the
746 // navigation bar would have caused.
747 ZeroSides zeroSides = ZeroSides.NONE;
748 if (!navigationBarVisible) {
749 zeroSides = calculateShouldZeroSides();
750 }
751
752 // Status bar (top), navigation bar (bottom) and left/right system insets should
753 // partially obscure the content (padding).
754 viewportMetrics.viewPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0;
755 viewportMetrics.viewPaddingRight =
756 zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
757 ? 0
758 : insets.getSystemWindowInsetRight();
759 viewportMetrics.viewPaddingBottom =
760 navigationBarVisible && guessBottomKeyboardInset(insets) == 0
761 ? insets.getSystemWindowInsetBottom()
762 : 0;
763 viewportMetrics.viewPaddingLeft =
764 zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
765 ? 0
766 : insets.getSystemWindowInsetLeft();
767
768 // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
769 viewportMetrics.viewInsetTop = 0;
770 viewportMetrics.viewInsetRight = 0;
771 viewportMetrics.viewInsetBottom = guessBottomKeyboardInset(insets);
772 viewportMetrics.viewInsetLeft = 0;
773 }
774
775 Log.v(
776 TAG,
777 "Updating window insets (onApplyWindowInsets()):\n"
778 + "Status bar insets: Top: "
779 + viewportMetrics.viewPaddingTop
780 + ", Left: "
781 + viewportMetrics.viewPaddingLeft
782 + ", Right: "
783 + viewportMetrics.viewPaddingRight
784 + "\n"
785 + "Keyboard insets: Bottom: "
786 + viewportMetrics.viewInsetBottom
787 + ", Left: "
788 + viewportMetrics.viewInsetLeft
789 + ", Right: "
790 + viewportMetrics.viewInsetRight
791 + "System Gesture Insets - Left: "
792 + viewportMetrics.systemGestureInsetLeft
793 + ", Top: "
794 + viewportMetrics.systemGestureInsetTop
795 + ", Right: "
796 + viewportMetrics.systemGestureInsetRight
797 + ", Bottom: "
798 + viewportMetrics.viewInsetBottom);
799
800 sendViewportMetricsToFlutter();
801 return newInsets;
802 }
803 // ------- End: Process View configuration that Flutter cares about. --------
804
805 // -------- Start: Process UI I/O that Flutter cares about. -------
806 /**
807 * Creates an {@link InputConnection} to work with a {@link
808 * android.view.inputmethod.InputMethodManager}.
809 *
810 * <p>Any {@code View} that can take focus or process text input must implement this method by
811 * returning a non-null {@code InputConnection}. Flutter may render one or many focusable and
812 * text-input widgets, therefore {@code FlutterView} must support an {@code InputConnection}.
813 *
814 * <p>The {@code InputConnection} returned from this method comes from a {@link TextInputPlugin},
815 * which is owned by this {@code FlutterView}. A {@link TextInputPlugin} exists to encapsulate the
816 * nuances of input communication, rather than spread that logic throughout this {@code
817 * FlutterView}.
818 */
819 @Override
820 @Nullable
821 public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
822 if (!isAttachedToFlutterEngine()) {
823 return super.onCreateInputConnection(outAttrs);
824 }
825
826 return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs);
827 }
828
829 /**
830 * Allows a {@code View} that is not currently the input connection target to invoke commands on
831 * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
832 *
833 * <p>Returns true to allow non-input-connection-targets to invoke methods on {@code
834 * InputMethodManager}, or false to exclusively allow the input connection target to invoke such
835 * methods.
836 */
837 @Override
838 public boolean checkInputConnectionProxy(View view) {
839 return flutterEngine != null
840 ? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
841 : super.checkInputConnectionProxy(view);
842 }
843
844 /**
845 * Invoked when a hardware key is pressed or released.
846 *
847 * <p>This method is typically invoked in response to the press of a physical keyboard key or a
848 * D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
849 * software keyboard may choose to invoke this method in some situations.
850 *
851 * <p>{@link KeyEvent}s are sent from Android to Flutter. {@link KeyboardManager} may do some
852 * additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the
853 * previous {@code keyCode} to generate a unicode combined character.
854 */
855 @Override
856 public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
857 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
858 // Tell Android to start tracking this event.
859 getKeyDispatcherState().startTracking(event, this);
860 } else if (event.getAction() == KeyEvent.ACTION_UP) {
861 // Stop tracking the event.
862 getKeyDispatcherState().handleUpEvent(event);
863 }
864 // If the key processor doesn't handle it, then send it on to the
865 // superclass. The key processor will typically handle all events except
866 // those where it has re-dispatched the event after receiving a reply from
867 // the framework that the framework did not handle it.
868 return (isAttachedToFlutterEngine() && keyboardManager.handleEvent(event))
869 || super.dispatchKeyEvent(event);
870 }
871
872 /**
873 * Invoked by Android when a user touch event occurs.
874 *
875 * <p>Flutter handles all of its own gesture detection and processing, therefore this method
876 * forwards all {@link MotionEvent} data from Android to Flutter.
877 */
878 @Override
879 public boolean onTouchEvent(@NonNull MotionEvent event) {
880 if (!isAttachedToFlutterEngine()) {
881 return super.onTouchEvent(event);
882 }
883
884 requestUnbufferedDispatch(event);
885
886 return androidTouchProcessor.onTouchEvent(event);
887 }
888
889 /**
890 * Invoked by Android when a generic motion event occurs, e.g., joystick movement, mouse hover,
891 * track pad touches, scroll wheel movements, etc.
892 *
893 * <p>Flutter handles all of its own gesture detection and processing, therefore this method
894 * forwards all {@link MotionEvent} data from Android to Flutter.
895 */
896 @Override
897 public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
898 boolean handled =
899 isAttachedToFlutterEngine()
900 && androidTouchProcessor.onGenericMotionEvent(event, getContext());
901 return handled ? true : super.onGenericMotionEvent(event);
902 }
903
904 /**
905 * Invoked by Android when a hover-compliant input system causes a hover event.
906 *
907 * <p>An example of hover events is a stylus sitting near an Android screen. As the stylus moves
908 * from outside a {@code View} to hover over a {@code View}, or move around within a {@code View},
909 * or moves from over a {@code View} to outside a {@code View}, a corresponding {@link
910 * MotionEvent} is reported via this method.
911 *
912 * <p>Hover events can be used for accessibility touch exploration and therefore are processed
913 * here for accessibility purposes.
914 */
915 @Override
916 public boolean onHoverEvent(@NonNull MotionEvent event) {
917 if (!isAttachedToFlutterEngine()) {
918 return super.onHoverEvent(event);
919 }
920
921 boolean handled = accessibilityBridge.onAccessibilityHoverEvent(event);
922 if (!handled) {
923 // TODO(ianh): Expose hover events to the platform,
924 // implementing ADD, REMOVE, etc.
925 }
926 return handled;
927 }
928 // -------- End: Process UI I/O that Flutter cares about. ---------
929
930 // -------- Start: Accessibility -------
931 @Override
932 @Nullable
933 public AccessibilityNodeProvider getAccessibilityNodeProvider() {
934 if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
935 return accessibilityBridge;
936 } else {
937 // TODO(goderbauer): when a11y is off this should return a one-off snapshot of
938 // the a11y
939 // tree.
940 return null;
941 }
942 }
943
944 /**
945 * Prior to Android Q, it's impossible to add real views as descendants of virtual nodes. This
946 * breaks accessibility when an Android view is embedded in a Flutter app.
947 *
948 * <p>This method overrides a hidden method in {@code ViewGroup} to workaround this limitation.
949 * This solution is derivated from Jetpack Compose, and can be found in the Android source code as
950 * well.
951 *
952 * <p>This workaround finds the descendant {@code View} that matches the provided accessibility
953 * id.
954 *
955 * @param accessibilityId The view accessibility id.
956 * @return The view matching the accessibility id if any.
957 */
958 @SuppressLint("SoonBlockedPrivateApi")
959 @Nullable
960 public View findViewByAccessibilityIdTraversal(int accessibilityId) {
961 if (Build.VERSION.SDK_INT < API_LEVELS.API_29) {
962 return findViewByAccessibilityIdRootedAtCurrentView(accessibilityId, this);
963 }
964 // Android Q or later doesn't call this method.
965 //
966 // However, since this is implementation detail, a future version of Android might call
967 // this method again, fallback to calling the This member is not intended for public use, and is
968 // only visible for testing. method as expected by ViewGroup.
969 Method findViewByAccessibilityIdTraversalMethod;
970 try {
971 findViewByAccessibilityIdTraversalMethod =
972 View.class.getDeclaredMethod("findViewByAccessibilityIdTraversal", int.class);
973 } catch (NoSuchMethodException exception) {
974 return null;
975 }
976 findViewByAccessibilityIdTraversalMethod.setAccessible(true);
977 try {
978 return (View) findViewByAccessibilityIdTraversalMethod.invoke(this, accessibilityId);
979 } catch (IllegalAccessException exception) {
980 return null;
981 } catch (InvocationTargetException exception) {
982 return null;
983 }
984 }
985
986 /**
987 * Finds the descendant view that matches the provided accessibility id.
988 *
989 * @param accessibilityId The view accessibility id.
990 * @param currentView The root view.
991 * @return A descendant of currentView or currentView itself.
992 */
993 @SuppressLint("DiscouragedPrivateApi")
994 private View findViewByAccessibilityIdRootedAtCurrentView(int accessibilityId, View currentView) {
995 Method getAccessibilityViewIdMethod;
996 try {
997 getAccessibilityViewIdMethod = View.class.getDeclaredMethod("getAccessibilityViewId");
998 } catch (NoSuchMethodException exception) {
999 return null;
1000 }
1001 getAccessibilityViewIdMethod.setAccessible(true);
1002 try {
1003 if (getAccessibilityViewIdMethod.invoke(currentView).equals(accessibilityId)) {
1004 return currentView;
1005 }
1006 } catch (IllegalAccessException exception) {
1007 return null;
1008 } catch (InvocationTargetException exception) {
1009 return null;
1010 }
1011 if (currentView instanceof ViewGroup) {
1012 for (int i = 0; i < ((ViewGroup) currentView).getChildCount(); i++) {
1013 View view =
1014 findViewByAccessibilityIdRootedAtCurrentView(
1015 accessibilityId, ((ViewGroup) currentView).getChildAt(i));
1016 if (view != null) {
1017 return view;
1018 }
1019 }
1020 }
1021 return null;
1022 }
1023
1024 // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise
1025 // add comments.
1026 private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
1027 if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) {
1028 setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
1029 } else {
1030 setWillNotDraw(false);
1031 }
1032 }
1033 // -------- End: Accessibility ---------
1034
1035 // -------- Start: Mouse -------
1036 @Override
1037 @TargetApi(API_LEVELS.API_24)
1038 @RequiresApi(API_LEVELS.API_24)
1039 @NonNull
1040 public PointerIcon getSystemPointerIcon(int type) {
1041 return PointerIcon.getSystemIcon(getContext(), type);
1042 }
1043 // -------- End: Mouse ---------
1044
1045 // -------- Start: Keyboard -------
1046
1047 @Override
1048 public BinaryMessenger getBinaryMessenger() {
1049 return flutterEngine.getDartExecutor();
1050 }
1051
1052 @Override
1053 public boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent) {
1054 return textInputPlugin.handleKeyEvent(keyEvent);
1055 }
1056
1057 @Override
1058 public void redispatch(@NonNull KeyEvent keyEvent) {
1059 getRootView().dispatchKeyEvent(keyEvent);
1060 }
1061
1062 // -------- End: Keyboard -------
1063
1064 /**
1065 * Connects this {@code FlutterView} to the given {@link
1066 * io.flutter.embedding.engine.FlutterEngine}.
1067 *
1068 * <p>This {@code FlutterView} will begin rendering the UI painted by the given {@link
1069 * FlutterEngine}. This {@code FlutterView} will also begin forwarding interaction events from
1070 * this {@code FlutterView} to the given {@link io.flutter.embedding.engine.FlutterEngine}, e.g.,
1071 * user touch events, accessibility events, keyboard events, and others.
1072 *
1073 * <p>See {@link #detachFromFlutterEngine()} for information on how to detach from a {@link
1074 * FlutterEngine}.
1075 */
1076 public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
1077 Log.v(TAG, "Attaching to a FlutterEngine: " + flutterEngine);
1078 if (isAttachedToFlutterEngine()) {
1079 if (flutterEngine == this.flutterEngine) {
1080 // We are already attached to this FlutterEngine
1081 Log.v(TAG, "Already attached to this engine. Doing nothing.");
1082 return;
1083 }
1084
1085 // Detach from a previous FlutterEngine so we can attach to this new one.
1086 Log.v(
1087 TAG,
1088 "Currently attached to a different engine. Detaching and then attaching"
1089 + " to new engine.");
1091 }
1092
1093 this.flutterEngine = flutterEngine;
1094
1095 // Instruct our FlutterRenderer that we are now its designated RenderSurface.
1096 FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
1097 isFlutterUiDisplayed = flutterRenderer.isDisplayingFlutterUi();
1098 renderSurface.attachToRenderer(flutterRenderer);
1099 flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
1100
1101 // Initialize various components that know how to process Android View I/O
1102 // in a way that Flutter understands.
1103 if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
1104 mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel());
1105 }
1107 new TextInputPlugin(
1108 this,
1109 this.flutterEngine.getTextInputChannel(),
1110 this.flutterEngine.getPlatformViewsController());
1111
1112 try {
1113 textServicesManager =
1114 (TextServicesManager)
1115 getContext().getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
1116 spellCheckPlugin =
1117 new SpellCheckPlugin(textServicesManager, this.flutterEngine.getSpellCheckChannel());
1118 } catch (Exception e) {
1119 Log.e(TAG, "TextServicesManager not supported by device, spell check disabled.");
1120 }
1121
1122 localizationPlugin = this.flutterEngine.getLocalizationPlugin();
1123
1124 keyboardManager = new KeyboardManager(this);
1125 androidTouchProcessor =
1126 new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false);
1127 accessibilityBridge =
1128 new AccessibilityBridge(
1129 this,
1130 flutterEngine.getAccessibilityChannel(),
1131 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
1132 getContext().getContentResolver(),
1133 this.flutterEngine.getPlatformViewsController());
1134 accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
1135 resetWillNotDraw(
1136 accessibilityBridge.isAccessibilityEnabled(),
1137 accessibilityBridge.isTouchExplorationEnabled());
1138
1139 // Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine.
1140 // This allows platform Views to hook into Flutter's overall accessibility system.
1141 this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge);
1142 this.flutterEngine
1143 .getPlatformViewsController()
1144 .attachToFlutterRenderer(this.flutterEngine.getRenderer());
1145
1146 // Inform the Android framework that it should retrieve a new InputConnection
1147 // now that an engine is attached.
1148 // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
1149 textInputPlugin.getInputMethodManager().restartInput(this);
1150
1151 // Push View and Context related information from Android to Flutter.
1152 sendUserSettingsToFlutter();
1153 getContext()
1154 .getContentResolver()
1155 .registerContentObserver(
1156 Settings.System.getUriFor(Settings.System.TEXT_SHOW_PASSWORD),
1157 false,
1158 systemSettingsObserver);
1159
1160 sendViewportMetricsToFlutter();
1161
1162 flutterEngine.getPlatformViewsController().attachToView(this);
1163
1164 // Notify engine attachment listeners of the attachment.
1165 for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
1166 listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
1167 }
1168
1169 // If the first frame has already been rendered, notify all first frame listeners.
1170 // Do this after all other initialization so that listeners don't inadvertently interact
1171 // with a FlutterView that is only partially attached to a FlutterEngine.
1172 if (isFlutterUiDisplayed) {
1173 flutterUiDisplayListener.onFlutterUiDisplayed();
1174 }
1175 }
1176
1177 /**
1178 * Disconnects this {@code FlutterView} from a previously attached {@link
1179 * io.flutter.embedding.engine.FlutterEngine}.
1180 *
1181 * <p>This {@code FlutterView} will clear its UI and stop forwarding all events to the
1182 * previously-attached {@link io.flutter.embedding.engine.FlutterEngine}. This includes touch
1183 * events, accessibility events, keyboard events, and others.
1184 *
1185 * <p>See {@link #attachToFlutterEngine(FlutterEngine)} for information on how to attach a {@link
1186 * FlutterEngine}.
1187 */
1189 Log.v(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
1190 if (!isAttachedToFlutterEngine()) {
1191 Log.v(TAG, "FlutterView not attached to an engine. Not detaching.");
1192 return;
1193 }
1194
1195 // Notify engine attachment listeners of the detachment.
1196 for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
1197 listener.onFlutterEngineDetachedFromFlutterView();
1198 }
1199
1200 getContext().getContentResolver().unregisterContentObserver(systemSettingsObserver);
1201
1202 flutterEngine.getPlatformViewsController().detachFromView();
1203
1204 // Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
1205 flutterEngine.getPlatformViewsController().detachAccessibilityBridge();
1206
1207 // Disconnect and clean up the AccessibilityBridge.
1208 accessibilityBridge.release();
1209 accessibilityBridge = null;
1210
1211 // Inform the Android framework that it should retrieve a new InputConnection
1212 // now that the engine is detached. The new InputConnection will be null, which
1213 // signifies that this View does not process input (until a new engine is attached).
1214 // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
1215 textInputPlugin.getInputMethodManager().restartInput(this);
1216 textInputPlugin.destroy();
1217 keyboardManager.destroy();
1218 if (spellCheckPlugin != null) {
1219 spellCheckPlugin.destroy();
1220 }
1221
1222 if (mouseCursorPlugin != null) {
1223 mouseCursorPlugin.destroy();
1224 }
1225
1226 // Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
1227 FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
1228 isFlutterUiDisplayed = false;
1229 flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
1230 flutterRenderer.stopRenderingToSurface();
1231 flutterRenderer.setSemanticsEnabled(false);
1232
1233 // Revert the image view to previous surface
1234 if (previousRenderSurface != null && renderSurface == flutterImageView) {
1235 renderSurface = previousRenderSurface;
1236 }
1237 renderSurface.detachFromRenderer();
1238
1239 releaseImageView();
1240
1241 previousRenderSurface = null;
1242 flutterEngine = null;
1243 }
1244
1245 private void releaseImageView() {
1246 if (flutterImageView != null) {
1247 flutterImageView.closeImageReader();
1248 // Remove the FlutterImageView that was previously added by {@code convertToImageView} to
1249 // avoid leaks when this FlutterView is reused later in the scenario where multiple
1250 // FlutterActivity/FlutterFragment share one engine.
1251 removeView(flutterImageView);
1252 flutterImageView = null;
1253 }
1254 }
1255
1256 @VisibleForTesting
1257 @NonNull
1259 return new FlutterImageView(
1260 getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background);
1261 }
1262
1263 @VisibleForTesting
1265 return flutterImageView;
1266 }
1267
1268 /**
1269 * Converts the current render surface to a {@link FlutterImageView} if it's not one already.
1270 * Otherwise, it resizes the {@link FlutterImageView} based on the current view size.
1271 */
1272 public void convertToImageView() {
1273 renderSurface.pause();
1274
1275 if (flutterImageView == null) {
1276 flutterImageView = createImageView();
1277 addView(flutterImageView);
1278 } else {
1279 flutterImageView.resizeIfNeeded(getWidth(), getHeight());
1280 }
1281
1282 previousRenderSurface = renderSurface;
1283 renderSurface = flutterImageView;
1284 if (flutterEngine != null) {
1285 renderSurface.attachToRenderer(flutterEngine.getRenderer());
1286 }
1287 }
1288
1289 /**
1290 * If the surface is rendered by a {@link FlutterImageView}, then calling this method will stop
1291 * rendering to a {@link FlutterImageView}, and render on the previous surface instead.
1292 *
1293 * @param onDone a callback called when Flutter UI is rendered on the previous surface. Use this
1294 * callback to perform cleanups. For example, destroy overlay surfaces.
1295 */
1296 public void revertImageView(@NonNull Runnable onDone) {
1297 if (flutterImageView == null) {
1298 Log.v(TAG, "Tried to revert the image view, but no image view is used.");
1299 return;
1300 }
1301 if (previousRenderSurface == null) {
1302 Log.v(TAG, "Tried to revert the image view, but no previous surface was used.");
1303 return;
1304 }
1305 renderSurface = previousRenderSurface;
1306 previousRenderSurface = null;
1307
1308 final FlutterRenderer renderer = flutterEngine.getRenderer();
1309
1310 if (flutterEngine == null || renderer == null) {
1311 flutterImageView.detachFromRenderer();
1312 releaseImageView();
1313 onDone.run();
1314 return;
1315 }
1316 // Resume rendering to the previous surface.
1317 // This surface is typically `FlutterSurfaceView` or `FlutterTextureView`.
1318 renderSurface.resume();
1319
1320 // Install a Flutter UI listener to wait until the first frame is rendered
1321 // in the new surface to call the `onDone` callback.
1322 renderer.addIsDisplayingFlutterUiListener(
1323 new FlutterUiDisplayListener() {
1324 @Override
1325 public void onFlutterUiDisplayed() {
1326 renderer.removeIsDisplayingFlutterUiListener(this);
1327 onDone.run();
1328 if (!(renderSurface instanceof FlutterImageView) && flutterImageView != null) {
1329 flutterImageView.detachFromRenderer();
1330 releaseImageView();
1331 }
1332 }
1333
1334 @Override
1335 public void onFlutterUiNoLongerDisplayed() {
1336 // no-op
1337 }
1338 });
1339 }
1340
1342 if (flutterEngine != null) {
1343 view.attachToRenderer(flutterEngine.getRenderer());
1344 }
1345 }
1346
1348 if (flutterImageView != null) {
1349 return flutterImageView.acquireLatestImage();
1350 }
1351 return false;
1352 }
1353
1354 /**
1355 * Returns true if this {@code FlutterView} is currently attached to a {@link
1356 * io.flutter.embedding.engine.FlutterEngine}.
1357 */
1358 @VisibleForTesting
1359 public boolean isAttachedToFlutterEngine() {
1360 return flutterEngine != null
1361 && flutterEngine.getRenderer() == renderSurface.getAttachedRenderer();
1362 }
1363
1364 /**
1365 * Returns the {@link io.flutter.embedding.engine.FlutterEngine} to which this {@code FlutterView}
1366 * is currently attached, or null if this {@code FlutterView} is not currently attached to a
1367 * {@link io.flutter.embedding.engine.FlutterEngine}.
1368 */
1369 @VisibleForTesting
1370 @Nullable
1371 public FlutterEngine getAttachedFlutterEngine() {
1372 return flutterEngine;
1373 }
1374
1375 /**
1376 * Adds a {@link FlutterEngineAttachmentListener}, which is notified whenever this {@code
1377 * FlutterView} attached to/detaches from a {@link io.flutter.embedding.engine.FlutterEngine}.
1378 */
1379 @VisibleForTesting
1380 public void addFlutterEngineAttachmentListener(
1381 @NonNull FlutterEngineAttachmentListener listener) {
1382 flutterEngineAttachmentListeners.add(listener);
1383 }
1384
1385 /**
1386 * Removes a {@link FlutterEngineAttachmentListener} that was previously added with {@link
1387 * #addFlutterEngineAttachmentListener(FlutterEngineAttachmentListener)}.
1388 */
1389 @VisibleForTesting
1390 public void removeFlutterEngineAttachmentListener(
1391 @NonNull FlutterEngineAttachmentListener listener) {
1392 flutterEngineAttachmentListeners.remove(listener);
1393 }
1394
1395 /**
1396 * Send various user preferences of this Android device to Flutter.
1397 *
1398 * <p>For example, sends the user's "text scale factor" preferences, as well as the user's clock
1399 * format preference.
1400 *
1401 * <p>FlutterEngine must be non-null when this method is invoked.
1402 */
1403 @VisibleForTesting
1404 /* package */ void sendUserSettingsToFlutter() {
1405 // Lookup the current brightness of the Android OS.
1406 boolean isNightModeOn =
1407 (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
1408 == Configuration.UI_MODE_NIGHT_YES;
1409 SettingsChannel.PlatformBrightness brightness =
1410 isNightModeOn
1411 ? SettingsChannel.PlatformBrightness.dark
1412 : SettingsChannel.PlatformBrightness.light;
1413
1414 boolean isNativeSpellCheckServiceDefined = false;
1415
1416 if (textServicesManager != null) {
1417 if (Build.VERSION.SDK_INT >= API_LEVELS.API_31) {
1418 List<SpellCheckerInfo> enabledSpellCheckerInfos =
1419 textServicesManager.getEnabledSpellCheckerInfos();
1420 boolean gboardSpellCheckerEnabled =
1421 enabledSpellCheckerInfos.stream()
1422 .anyMatch(
1423 spellCheckerInfo ->
1424 spellCheckerInfo
1425 .getPackageName()
1426 .equals("com.google.android.inputmethod.latin"));
1427
1428 // Checks if enabled spell checker is the one that is suppported by Gboard, which is
1429 // the one Flutter supports by default.
1430 isNativeSpellCheckServiceDefined =
1431 textServicesManager.isSpellCheckerEnabled() && gboardSpellCheckerEnabled;
1432 } else {
1433 isNativeSpellCheckServiceDefined = true;
1434 }
1435 }
1436
1437 flutterEngine
1438 .getSettingsChannel()
1439 .startMessage()
1440 .setTextScaleFactor(getResources().getConfiguration().fontScale)
1441 .setDisplayMetrics(getResources().getDisplayMetrics())
1442 .setNativeSpellCheckServiceDefined(isNativeSpellCheckServiceDefined)
1443 .setBrieflyShowPassword(
1444 Settings.System.getInt(
1445 getContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1)
1446 == 1)
1447 .setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
1448 .setPlatformBrightness(brightness)
1449 .send();
1450 }
1451
1452 private void sendViewportMetricsToFlutter() {
1453 if (!isAttachedToFlutterEngine()) {
1454 Log.w(
1455 TAG,
1456 "Tried to send viewport metrics from Android to Flutter but this "
1457 + "FlutterView was not attached to a FlutterEngine.");
1458 return;
1459 }
1460
1461 viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density;
1462 viewportMetrics.physicalTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
1463 flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
1464 }
1465
1466 @Override
1467 public void onProvideAutofillVirtualStructure(@NonNull ViewStructure structure, int flags) {
1468 super.onProvideAutofillVirtualStructure(structure, flags);
1469 textInputPlugin.onProvideAutofillVirtualStructure(structure, flags);
1470 }
1471
1472 @Override
1473 public void autofill(@NonNull SparseArray<AutofillValue> values) {
1474 textInputPlugin.autofill(values);
1475 }
1476
1477 @Override
1478 public void setVisibility(int visibility) {
1479 super.setVisibility(visibility);
1480 // For `FlutterSurfaceView`, setting visibility to the current `FlutterView` will not take
1481 // effect since it is not in the view tree. So override this method and set the surfaceView.
1482 // See https://github.com/flutter/flutter/issues/105203
1483 if (renderSurface instanceof FlutterSurfaceView) {
1484 ((FlutterSurfaceView) renderSurface).setVisibility(visibility);
1485 }
1486 }
1487
1488 /**
1489 * Listener that is notified when a {@link io.flutter.embedding.engine.FlutterEngine} is attached
1490 * to/detached from a given {@code FlutterView}.
1491 */
1492 @VisibleForTesting
1493 public interface FlutterEngineAttachmentListener {
1494 /** The given {@code engine} has been attached to the associated {@code FlutterView}. */
1495 void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine);
1496
1497 /**
1498 * A previously attached {@link io.flutter.embedding.engine.FlutterEngine} has been detached
1499 * from the associated {@code FlutterView}.
1500 */
1501 void onFlutterEngineDetachedFromFlutterView();
1502 }
1503}
static bool equals(T *a, T *b)
static void v(@NonNull String tag, @NonNull String message)
Definition Log.java:40
static void e(@NonNull String tag, @NonNull String message)
Definition Log.java:84
View findViewByAccessibilityIdTraversal(int accessibilityId)
FlutterView(@NonNull Context context, @NonNull RenderMode renderMode)
InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs)
FlutterView(@NonNull Context context)
boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent)
void removeOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener)
FlutterView(@NonNull Context context, @NonNull TransparencyMode transparencyMode)
void onConfigurationChanged(@NonNull Configuration newConfig)
FlutterView(@NonNull Context context, @NonNull FlutterSurfaceView flutterSurfaceView)
FlutterView(@NonNull Context context, @NonNull FlutterImageView flutterImageView)
boolean onTouchEvent(@NonNull MotionEvent event)
final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets)
FlutterView( @NonNull Context context, @NonNull RenderMode renderMode, @NonNull TransparencyMode transparencyMode)
void addOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener)
boolean onHoverEvent(@NonNull MotionEvent event)
boolean dispatchKeyEvent(@NonNull KeyEvent event)
void attachOverlaySurfaceToRender(@NonNull FlutterImageView view)
void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine)
boolean onGenericMotionEvent(@NonNull MotionEvent event)
void setWindowInfoListenerDisplayFeatures(WindowLayoutInfo layoutInfo)
void redispatch(@NonNull KeyEvent keyEvent)
WindowInfoRepositoryCallbackAdapterWrapper createWindowInfoRepo()
FlutterView(@NonNull Context context, @Nullable AttributeSet attrs)
void revertImageView(@NonNull Runnable onDone)
void onSizeChanged(int width, int height, int oldWidth, int oldHeight)
AccessibilityNodeProvider getAccessibilityNodeProvider()
FlutterView(@NonNull Context context, @NonNull FlutterTextureView flutterTextureView)
FlutterEngine engine
Definition main.cc:68
AtkStateType state
FlutterSemanticsFlag flags
FlKeyEvent * event
GAsyncResult * result
FlutterTextInputPlugin * textInputPlugin
void Log(const char *format,...) SK_PRINTF_LIKE(1
Build(configs, env, options)
Definition build.py:232
#define TAG()
int32_t height
int32_t width