• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
5 package io.flutter.embedding.android;
6 
7 import android.annotation.TargetApi;
8 import android.annotation.SuppressLint;
9 import android.content.Context;
10 import android.content.res.Configuration;
11 import android.graphics.Insets;
12 import android.graphics.Rect;
13 import android.os.Build;
14 import android.os.LocaleList;
15 import android.support.annotation.NonNull;
16 import android.support.annotation.Nullable;
17 import android.support.annotation.RequiresApi;
18 import android.support.annotation.VisibleForTesting;
19 import android.text.format.DateFormat;
20 import android.util.AttributeSet;
21 import android.view.KeyEvent;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.WindowInsets;
25 import android.view.accessibility.AccessibilityManager;
26 import android.view.accessibility.AccessibilityNodeProvider;
27 import android.view.inputmethod.EditorInfo;
28 import android.view.inputmethod.InputConnection;
29 import android.widget.FrameLayout;
30 
31 import java.util.ArrayList;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Set;
36 
37 import io.flutter.Log;
38 import io.flutter.embedding.engine.FlutterEngine;
39 import io.flutter.embedding.engine.renderer.FlutterRenderer;
40 import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
41 import io.flutter.plugin.editing.TextInputPlugin;
42 import io.flutter.plugin.platform.PlatformViewsController;
43 import io.flutter.view.AccessibilityBridge;
44 
45 /**
46  * Displays a Flutter UI on an Android device.
47  * <p>
48  * A {@code FlutterView}'s UI is painted by a corresponding {@link FlutterEngine}.
49  * <p>
50  * A {@code FlutterView} can operate in 2 different {@link RenderMode}s:
51  * <ol>
52  *   <li>{@link RenderMode#surface}, which paints a Flutter UI to a {@link android.view.SurfaceView}.
53  *   This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned
54  *   between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed.
55  *   Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required,
56  *   developers should strongly prefer this render mode.</li>
57  *   <li>{@link RenderMode#texture}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}.
58  *   This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this
59  *   mode can be animated and transformed, as well as positioned in the z-index between 2+ other
60  *   Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture}
61  *   are required, developers should strongly prefer the {@link RenderMode#surface} render mode.</li>
62  * </ol>
63  * See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more
64  * information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}.
65  */
66 public class FlutterView extends FrameLayout {
67   private static final String TAG = "FlutterView";
68 
69   // Behavior configuration of this FlutterView.
70   @NonNull
71   private RenderMode renderMode;
72   @Nullable
73   private TransparencyMode transparencyMode;
74 
75   // Internal view hierarchy references.
76   @Nullable
77   private FlutterRenderer.RenderSurface renderSurface;
78   private final Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();
79   private boolean didRenderFirstFrame;
80 
81   // Connections to a Flutter execution context.
82   @Nullable
83   private FlutterEngine flutterEngine;
84   @NonNull
85   private final Set<FlutterEngineAttachmentListener> flutterEngineAttachmentListeners = new HashSet<>();
86 
87   // Components that process various types of Android View input and events,
88   // possibly storing intermediate state, and communicating those events to Flutter.
89   //
90   // These components essentially add some additional behavioral logic on top of
91   // existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc.
92   @Nullable
93   private TextInputPlugin textInputPlugin;
94   @Nullable
95   private AndroidKeyProcessor androidKeyProcessor;
96   @Nullable
97   private AndroidTouchProcessor androidTouchProcessor;
98   @Nullable
99   private AccessibilityBridge accessibilityBridge;
100 
101   // Directly implemented View behavior that communicates with Flutter.
102   private final FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics();
103 
104   private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener = new AccessibilityBridge.OnAccessibilityChangeListener() {
105     @Override
106     public void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
107       resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
108     }
109   };
110 
111   private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
112     @Override
113     public void onFirstFrameRendered() {
114       didRenderFirstFrame = true;
115 
116       for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
117         listener.onFirstFrameRendered();
118       }
119     }
120   };
121 
122   /**
123    * Constructs a {@code FlutterView} programmatically, without any XML attributes.
124    * <p>
125    * <ul>
126    *   <li>{@link #renderMode} defaults to {@link RenderMode#surface}.</li>
127    *   <li>{@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.</li>
128    * </ul>
129    * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
130    * to be compatible with {@link PlatformViewsController}.
131    */
FlutterView(@onNull Context context)132   public FlutterView(@NonNull Context context) {
133     this(context, null, null, null);
134   }
135 
136   /**
137    * Constructs a {@code FlutterView} programmatically, without any XML attributes,
138    * and allows selection of a {@link #renderMode}.
139    * <p>
140    * {@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.
141    * <p>
142    * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
143    * to be compatible with {@link PlatformViewsController}.
144    */
FlutterView(@onNull Context context, @NonNull RenderMode renderMode)145   public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) {
146     this(context, null, renderMode, null);
147   }
148 
149   /**
150    * Constructs a {@code FlutterView} programmatically, without any XML attributes,
151    * assumes the use of {@link RenderMode#surface}, and allows selection of a {@link #transparencyMode}.
152    * <p>
153    * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
154    * to be compatible with {@link PlatformViewsController}.
155    */
FlutterView(@onNull Context context, @NonNull TransparencyMode transparencyMode)156   public FlutterView(@NonNull Context context, @NonNull TransparencyMode transparencyMode) {
157     this(context, null, RenderMode.surface, transparencyMode);
158   }
159 
160   /**
161    * Constructs a {@code FlutterView} programmatically, without any XML attributes, and allows
162    * a selection of {@link #renderMode} and {@link #transparencyMode}.
163    * <p>
164    * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
165    * to be compatible with {@link PlatformViewsController}.
166    */
FlutterView(@onNull Context context, @NonNull RenderMode renderMode, @NonNull TransparencyMode transparencyMode)167   public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode, @NonNull TransparencyMode transparencyMode) {
168     this(context, null, renderMode, transparencyMode);
169   }
170 
171   /**
172    * Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner.
173    * <p>
174    * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
175    * to be compatible with {@link PlatformViewsController}.
176    */
177    // TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
FlutterView(@onNull Context context, @Nullable AttributeSet attrs)178   public FlutterView(@NonNull Context context, @Nullable AttributeSet attrs) {
179     this(context, attrs, null, null);
180   }
181 
FlutterView(@onNull Context context, @Nullable AttributeSet attrs, @Nullable RenderMode renderMode, @Nullable TransparencyMode transparencyMode)182   private FlutterView(@NonNull Context context, @Nullable AttributeSet attrs, @Nullable RenderMode renderMode, @Nullable TransparencyMode transparencyMode) {
183     super(context, attrs);
184 
185     this.renderMode = renderMode == null ? RenderMode.surface : renderMode;
186     this.transparencyMode = transparencyMode != null ? transparencyMode : TransparencyMode.opaque;
187 
188     init();
189   }
190 
init()191   private void init() {
192     Log.v(TAG, "Initializing FlutterView");
193 
194     switch (renderMode) {
195       case surface:
196         Log.v(TAG, "Internally using a FlutterSurfaceView.");
197         FlutterSurfaceView flutterSurfaceView = new FlutterSurfaceView(getContext(), transparencyMode == TransparencyMode.transparent);
198         renderSurface = flutterSurfaceView;
199         addView(flutterSurfaceView);
200         break;
201       case texture:
202         Log.v(TAG, "Internally using a FlutterTextureView.");
203         FlutterTextureView flutterTextureView = new FlutterTextureView(getContext());
204         renderSurface = flutterTextureView;
205         addView(flutterTextureView);
206         break;
207     }
208 
209     // FlutterView needs to be focusable so that the InputMethodManager can interact with it.
210     setFocusable(true);
211     setFocusableInTouchMode(true);
212   }
213 
214   /**
215    * Returns true if an attached {@link FlutterEngine} has rendered at least 1 frame to this
216    * {@code FlutterView}.
217    * <p>
218    * Returns false if no {@link FlutterEngine} is attached.
219    * <p>
220    * This flag is specific to a given {@link FlutterEngine}. The following hypothetical timeline
221    * demonstrates how this flag changes over time.
222    * <ol>
223    *   <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false</li>
224    *   <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true</li>
225    *   <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false</li>
226    *   <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false</li>
227    *   <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true</li>
228    * </ol>
229    */
hasRenderedFirstFrame()230   public boolean hasRenderedFirstFrame() {
231     return didRenderFirstFrame;
232   }
233 
234   /**
235    * Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
236    * first rendered frame.
237    */
addOnFirstFrameRenderedListener(@onNull OnFirstFrameRenderedListener listener)238   public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
239     onFirstFrameRenderedListeners.add(listener);
240   }
241 
242   /**
243    * Removes the given {@code listener}, which was previously added with
244    * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
245    */
removeOnFirstFrameRenderedListener(@onNull OnFirstFrameRenderedListener listener)246   public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
247     onFirstFrameRenderedListeners.remove(listener);
248   }
249 
250   //------- Start: Process View configuration that Flutter cares about. ------
251   /**
252    * Sends relevant configuration data from Android to Flutter when the Android
253    * {@link Configuration} changes.
254    *
255    * The Android {@link Configuration} might change as a result of device orientation
256    * change, device language change, device text scale factor change, etc.
257    */
258   @Override
onConfigurationChanged(@onNull Configuration newConfig)259   protected void onConfigurationChanged(@NonNull Configuration newConfig) {
260     super.onConfigurationChanged(newConfig);
261     Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter.");
262     sendLocalesToFlutter(newConfig);
263     sendUserSettingsToFlutter();
264   }
265 
266   /**
267    * Invoked when this {@code FlutterView} changes size, including upon initial
268    * measure.
269    *
270    * The initial measure reports an {@code oldWidth} and {@code oldHeight} of zero.
271    *
272    * Flutter cares about the width and height of the view that displays it on the host
273    * platform. Therefore, when this method is invoked, the new width and height are
274    * communicated to Flutter as the "physical size" of the view that displays Flutter's
275    * UI.
276    */
277   @Override
onSizeChanged(int width, int height, int oldWidth, int oldHeight)278   protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
279     super.onSizeChanged(width, height, oldWidth, oldHeight);
280     Log.v(TAG, "Size changed. Sending Flutter new viewport metrics. FlutterView was "
281         + oldWidth + " x " + oldHeight
282         + ", it is now " + width + " x " + height);
283     viewportMetrics.width = width;
284     viewportMetrics.height = height;
285     sendViewportMetricsToFlutter();
286   }
287 
288   /**
289    * Invoked when Android's desired window insets change, i.e., padding.
290    *
291    * Flutter does not use a standard {@code View} hierarchy and therefore Flutter is
292    * unaware of these insets. Therefore, this method calculates the viewport metrics
293    * that Flutter should use and then sends those metrics to Flutter.
294    *
295    * This callback is not present in API < 20, which means lower API devices will see
296    * the wider than expected padding when the status and navigation bars are hidden.
297    */
298   @Override
299   @TargetApi(20)
300   @RequiresApi(20)
301   // The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings
302   // caused by usage of Android Q APIs. These calls are safe because they are
303   // guarded.
304   @SuppressLint({"InlinedApi", "NewApi"})
305   @NonNull
onApplyWindowInsets(@onNull WindowInsets insets)306   public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
307     WindowInsets newInsets = super.onApplyWindowInsets(insets);
308 
309     // Status bar (top) and left/right system insets should partially obscure the content (padding).
310     viewportMetrics.paddingTop = insets.getSystemWindowInsetTop();
311     viewportMetrics.paddingRight = insets.getSystemWindowInsetRight();
312     viewportMetrics.paddingBottom = 0;
313     viewportMetrics.paddingLeft = insets.getSystemWindowInsetLeft();
314 
315     // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
316     viewportMetrics.viewInsetTop = 0;
317     viewportMetrics.viewInsetRight = 0;
318     viewportMetrics.viewInsetBottom = insets.getSystemWindowInsetBottom();
319     viewportMetrics.viewInsetLeft = 0;
320 
321     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
322       Insets systemGestureInsets = insets.getSystemGestureInsets();
323       viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
324       viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
325       viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
326       viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
327     }
328 
329     Log.v(TAG, "Updating window insets (onApplyWindowInsets()):\n"
330       + "Status bar insets: Top: " + viewportMetrics.paddingTop
331         + ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
332       + "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
333         + ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight
334       + "System Gesture Insets - Left: " + viewportMetrics.systemGestureInsetLeft + ", Top: " + viewportMetrics.systemGestureInsetTop
335         + ", Right: " + viewportMetrics.systemGestureInsetRight + ", Bottom: " + viewportMetrics.viewInsetBottom);
336 
337     sendViewportMetricsToFlutter();
338 
339     return newInsets;
340   }
341 
342   /**
343    * Invoked when Android's desired window insets change, i.e., padding.
344    *
345    * {@code fitSystemWindows} is an earlier version of
346    * {@link #onApplyWindowInsets(WindowInsets)}. See that method for more details
347    * about how window insets relate to Flutter.
348    */
349   @Override
350   @SuppressWarnings("deprecation")
fitSystemWindows(@onNull Rect insets)351   protected boolean fitSystemWindows(@NonNull Rect insets) {
352     if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
353       // Status bar, left/right system insets partially obscure content (padding).
354       viewportMetrics.paddingTop = insets.top;
355       viewportMetrics.paddingRight = insets.right;
356       viewportMetrics.paddingBottom = 0;
357       viewportMetrics.paddingLeft = insets.left;
358 
359       // Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
360       viewportMetrics.viewInsetTop = 0;
361       viewportMetrics.viewInsetRight = 0;
362       viewportMetrics.viewInsetBottom = insets.bottom;
363       viewportMetrics.viewInsetLeft = 0;
364 
365       Log.v(TAG, "Updating window insets (fitSystemWindows()):\n"
366           + "Status bar insets: Top: " + viewportMetrics.paddingTop
367           + ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
368           + "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
369           + ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight);
370 
371       sendViewportMetricsToFlutter();
372       return true;
373     } else {
374       return super.fitSystemWindows(insets);
375     }
376   }
377   //------- End: Process View configuration that Flutter cares about. --------
378 
379   //-------- Start: Process UI I/O that Flutter cares about. -------
380   /**
381    * Creates an {@link InputConnection} to work with a {@link android.view.inputmethod.InputMethodManager}.
382    *
383    * Any {@code View} that can take focus or process text input must implement this
384    * method by returning a non-null {@code InputConnection}. Flutter may render one or
385    * many focusable and text-input widgets, therefore {@code FlutterView} must support
386    * an {@code InputConnection}.
387    *
388    * The {@code InputConnection} returned from this method comes from a
389    * {@link TextInputPlugin}, which is owned by this {@code FlutterView}. A
390    * {@link TextInputPlugin} exists to encapsulate the nuances of input communication,
391    * rather than spread that logic throughout this {@code FlutterView}.
392    */
393   @Override
394   @Nullable
onCreateInputConnection(@onNull EditorInfo outAttrs)395   public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
396     if (!isAttachedToFlutterEngine()) {
397       return super.onCreateInputConnection(outAttrs);
398     }
399 
400     return textInputPlugin.createInputConnection(this, outAttrs);
401   }
402 
403   /**
404    * Allows a {@code View} that is not currently the input connection target to invoke commands on
405    * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
406    * <p>
407    * Returns true to allow non-input-connection-targets to invoke methods on
408    * {@code InputMethodManager}, or false to exclusively allow the input connection target to invoke
409    * such methods.
410    */
411   @Override
checkInputConnectionProxy(View view)412   public boolean checkInputConnectionProxy(View view) {
413     return flutterEngine != null
414         ? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
415         : super.checkInputConnectionProxy(view);
416   }
417 
418   /**
419    * Invoked when key is released.
420    *
421    * This method is typically invoked in response to the release of a physical
422    * keyboard key or a D-pad button. It is generally not invoked when a virtual
423    * software keyboard is used, though a software keyboard may choose to invoke
424    * this method in some situations.
425    *
426    * {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
427    * may do some additional work with the given {@link KeyEvent}, e.g., combine this
428    * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
429    * character.
430    */
431   @Override
onKeyUp(int keyCode, @NonNull KeyEvent event)432   public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
433     if (!isAttachedToFlutterEngine()) {
434       return super.onKeyUp(keyCode, event);
435     }
436 
437     androidKeyProcessor.onKeyUp(event);
438     return super.onKeyUp(keyCode, event);
439   }
440 
441   /**
442    * Invoked when key is pressed.
443    *
444    * This method is typically invoked in response to the press of a physical
445    * keyboard key or a D-pad button. It is generally not invoked when a virtual
446    * software keyboard is used, though a software keyboard may choose to invoke
447    * this method in some situations.
448    *
449    * {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor}
450    * may do some additional work with the given {@link KeyEvent}, e.g., combine this
451    * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
452    * character.
453    */
454   @Override
onKeyDown(int keyCode, @NonNull KeyEvent event)455   public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
456     if (!isAttachedToFlutterEngine()) {
457       return super.onKeyDown(keyCode, event);
458     }
459 
460     androidKeyProcessor.onKeyDown(event);
461     return super.onKeyDown(keyCode, event);
462   }
463 
464   /**
465    * Invoked by Android when a user touch event occurs.
466    *
467    * Flutter handles all of its own gesture detection and processing, therefore this
468    * method forwards all {@link MotionEvent} data from Android to Flutter.
469    */
470   @Override
onTouchEvent(@onNull MotionEvent event)471   public boolean onTouchEvent(@NonNull MotionEvent event) {
472     if (!isAttachedToFlutterEngine()) {
473       return super.onTouchEvent(event);
474     }
475 
476     // TODO(abarth): This version check might not be effective in some
477     // versions of Android that statically compile code and will be upset
478     // at the lack of |requestUnbufferedDispatch|. Instead, we should factor
479     // version-dependent code into separate classes for each supported
480     // version and dispatch dynamically.
481     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
482       requestUnbufferedDispatch(event);
483     }
484 
485     return androidTouchProcessor.onTouchEvent(event);
486   }
487 
488   /**
489    * Invoked by Android when a generic motion event occurs, e.g., joystick movement, mouse hover,
490    * track pad touches, scroll wheel movements, etc.
491    *
492    * Flutter handles all of its own gesture detection and processing, therefore this
493    * method forwards all {@link MotionEvent} data from Android to Flutter.
494    */
495   @Override
onGenericMotionEvent(@onNull MotionEvent event)496   public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
497     boolean handled = isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
498     return handled ? true : super.onGenericMotionEvent(event);
499   }
500 
501   /**
502    * Invoked by Android when a hover-compliant input system causes a hover event.
503    *
504    * An example of hover events is a stylus sitting near an Android screen. As the
505    * stylus moves from outside a {@code View} to hover over a {@code View}, or move
506    * around within a {@code View}, or moves from over a {@code View} to outside a
507    * {@code View}, a corresponding {@link MotionEvent} is reported via this method.
508    *
509    * Hover events can be used for accessibility touch exploration and therefore are
510    * processed here for accessibility purposes.
511    */
512   @Override
onHoverEvent(@onNull MotionEvent event)513   public boolean onHoverEvent(@NonNull MotionEvent event) {
514     if (!isAttachedToFlutterEngine()) {
515       return super.onHoverEvent(event);
516     }
517 
518     boolean handled = accessibilityBridge.onAccessibilityHoverEvent(event);
519     if (!handled) {
520       // TODO(ianh): Expose hover events to the platform,
521       // implementing ADD, REMOVE, etc.
522     }
523     return handled;
524   }
525   //-------- End: Process UI I/O that Flutter cares about. ---------
526 
527   //-------- Start: Accessibility -------
528   @Override
529   @Nullable
getAccessibilityNodeProvider()530   public AccessibilityNodeProvider getAccessibilityNodeProvider() {
531     if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
532       return accessibilityBridge;
533     } else {
534       // TODO(goderbauer): when a11y is off this should return a one-off snapshot of
535       // the a11y
536       // tree.
537       return null;
538     }
539   }
540 
541   // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments.
resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled)542   private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
543     if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) {
544       setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
545     } else {
546       setWillNotDraw(false);
547     }
548   }
549   //-------- End: Accessibility ---------
550 
551   /**
552    * Connects this {@code FlutterView} to the given {@link FlutterEngine}.
553    *
554    * This {@code FlutterView} will begin rendering the UI painted by the given {@link FlutterEngine}.
555    * This {@code FlutterView} will also begin forwarding interaction events from this
556    * {@code FlutterView} to the given {@link FlutterEngine}, e.g., user touch events, accessibility
557    * events, keyboard events, and others.
558    *
559    * See {@link #detachFromFlutterEngine()} for information on how to detach from a
560    * {@link FlutterEngine}.
561    */
attachToFlutterEngine( @onNull FlutterEngine flutterEngine )562   public void attachToFlutterEngine(
563       @NonNull FlutterEngine flutterEngine
564   ) {
565     Log.d(TAG, "Attaching to a FlutterEngine: " + flutterEngine);
566     if (isAttachedToFlutterEngine()) {
567       if (flutterEngine == this.flutterEngine) {
568         // We are already attached to this FlutterEngine
569         Log.d(TAG, "Already attached to this engine. Doing nothing.");
570         return;
571       }
572 
573       // Detach from a previous FlutterEngine so we can attach to this new one.
574       Log.d(TAG, "Currently attached to a different engine. Detaching and then attaching"
575           + " to new engine.");
576       detachFromFlutterEngine();
577     }
578 
579     this.flutterEngine = flutterEngine;
580 
581     // Instruct our FlutterRenderer that we are now its designated RenderSurface.
582     FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
583     didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame();
584     flutterRenderer.attachToRenderSurface(renderSurface);
585     flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
586 
587     // Initialize various components that know how to process Android View I/O
588     // in a way that Flutter understands.
589     textInputPlugin = new TextInputPlugin(
590         this,
591         this.flutterEngine.getDartExecutor(),
592         this.flutterEngine.getPlatformViewsController()
593     );
594     androidKeyProcessor = new AndroidKeyProcessor(
595         this.flutterEngine.getKeyEventChannel(),
596         textInputPlugin
597     );
598     androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
599     accessibilityBridge = new AccessibilityBridge(
600         this,
601         flutterEngine.getAccessibilityChannel(),
602         (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
603         getContext().getContentResolver(),
604         this.flutterEngine.getPlatformViewsController()
605     );
606     accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
607     resetWillNotDraw(
608         accessibilityBridge.isAccessibilityEnabled(),
609         accessibilityBridge.isTouchExplorationEnabled()
610     );
611 
612     // Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine.
613     // This allows platform Views to hook into Flutter's overall accessibility system.
614     this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge);
615 
616     // Inform the Android framework that it should retrieve a new InputConnection
617     // now that an engine is attached.
618     // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
619     textInputPlugin.getInputMethodManager().restartInput(this);
620 
621     // Push View and Context related information from Android to Flutter.
622     sendUserSettingsToFlutter();
623     sendLocalesToFlutter(getResources().getConfiguration());
624     sendViewportMetricsToFlutter();
625 
626     // Notify engine attachment listeners of the attachment.
627     for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
628       listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
629     }
630 
631     // If the first frame has already been rendered, notify all first frame listeners.
632     // Do this after all other initialization so that listeners don't inadvertently interact
633     // with a FlutterView that is only partially attached to a FlutterEngine.
634     if (didRenderFirstFrame) {
635       onFirstFrameRenderedListener.onFirstFrameRendered();
636     }
637   }
638 
639   /**
640    * Disconnects this {@code FlutterView} from a previously attached {@link FlutterEngine}.
641    *
642    * This {@code FlutterView} will clear its UI and stop forwarding all events to the previously-attached
643    * {@link FlutterEngine}. This includes touch events, accessibility events, keyboard events,
644    * and others.
645    *
646    * See {@link #attachToFlutterEngine(FlutterEngine)} for information on how to attach a
647    * {@link FlutterEngine}.
648    */
detachFromFlutterEngine()649   public void detachFromFlutterEngine() {
650     Log.d(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
651     if (!isAttachedToFlutterEngine()) {
652       Log.d(TAG, "Not attached to an engine. Doing nothing.");
653       return;
654     }
655 
656     // Notify engine attachment listeners of the detachment.
657     for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
658       listener.onFlutterEngineDetachedFromFlutterView();
659     }
660 
661     // Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
662     flutterEngine.getPlatformViewsController().detachAccessibiltyBridge();
663 
664     // Disconnect and clean up the AccessibilityBridge.
665     accessibilityBridge.release();
666     accessibilityBridge = null;
667 
668     // Inform the Android framework that it should retrieve a new InputConnection
669     // now that the engine is detached. The new InputConnection will be null, which
670     // signifies that this View does not process input (until a new engine is attached).
671     // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
672     textInputPlugin.getInputMethodManager().restartInput(this);
673     textInputPlugin.destroy();
674 
675     // Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
676     FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
677     didRenderFirstFrame = false;
678     flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
679     flutterRenderer.detachFromRenderSurface();
680     flutterEngine = null;
681   }
682 
683   /**
684    * Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}.
685    */
686   @VisibleForTesting
isAttachedToFlutterEngine()687   public boolean isAttachedToFlutterEngine() {
688     return flutterEngine != null && flutterEngine.getRenderer().isAttachedTo(renderSurface);
689   }
690 
691   /**
692    * Returns the {@link FlutterEngine} to which this {@code FlutterView} is currently attached,
693    * or null if this {@code FlutterView} is not currently attached to a {@link FlutterEngine}.
694    */
695   @VisibleForTesting
696   @Nullable
getAttachedFlutterEngine()697   public FlutterEngine getAttachedFlutterEngine() {
698     return flutterEngine;
699   }
700 
701   /**
702    * Adds a {@link FlutterEngineAttachmentListener}, which is notifed whenever this {@code FlutterView}
703    * attached to/detaches from a {@link FlutterEngine}.
704    */
705   @VisibleForTesting
addFlutterEngineAttachmentListener(@onNull FlutterEngineAttachmentListener listener)706   public void addFlutterEngineAttachmentListener(@NonNull FlutterEngineAttachmentListener listener) {
707     flutterEngineAttachmentListeners.add(listener);
708   }
709 
710   /**
711    * Removes a {@link FlutterEngineAttachmentListener} that was previously added with
712    * {@link #addFlutterEngineAttachmentListener(FlutterEngineAttachmentListener)}.
713    */
714   @VisibleForTesting
removeFlutterEngineAttachmentListener(@onNull FlutterEngineAttachmentListener listener)715   public void removeFlutterEngineAttachmentListener(@NonNull FlutterEngineAttachmentListener listener) {
716     flutterEngineAttachmentListeners.remove(listener);
717   }
718 
719   /**
720    * Send the current {@link Locale} configuration to Flutter.
721    *
722    * FlutterEngine must be non-null when this method is invoked.
723    */
724   @SuppressWarnings("deprecation")
sendLocalesToFlutter(@onNull Configuration config)725   private void sendLocalesToFlutter(@NonNull Configuration config) {
726     List<Locale> locales = new ArrayList<>();
727     if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
728       LocaleList localeList = config.getLocales();
729       int localeCount = localeList.size();
730       for (int index = 0; index < localeCount; ++index) {
731         Locale locale = localeList.get(index);
732         locales.add(locale);
733       }
734     } else {
735       locales.add(config.locale);
736     }
737     flutterEngine.getLocalizationChannel().sendLocales(locales);
738   }
739 
740   /**
741    * Send various user preferences of this Android device to Flutter.
742    *
743    * For example, sends the user's "text scale factor" preferences, as well as the user's clock
744    * format preference.
745    *
746    * FlutterEngine must be non-null when this method is invoked.
747    */
sendUserSettingsToFlutter()748   private void sendUserSettingsToFlutter() {
749     flutterEngine.getSettingsChannel().startMessage()
750         .setTextScaleFactor(getResources().getConfiguration().fontScale)
751         .setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
752         .send();
753   }
754 
755   // TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI
sendViewportMetricsToFlutter()756   private void sendViewportMetricsToFlutter() {
757     if (!isAttachedToFlutterEngine()) {
758       Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this "
759           + "FlutterView was not attached to a FlutterEngine.");
760       return;
761     }
762 
763     viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density;
764     flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
765   }
766 
767   /**
768    * Render modes for a {@link FlutterView}.
769    */
770   public enum RenderMode {
771     /**
772      * {@code RenderMode}, which paints a Flutter UI to a {@link android.view.SurfaceView}.
773      * This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned
774      * between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed.
775      * Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required,
776      * developers should strongly prefer this render mode.
777      */
778     surface,
779     /**
780      * {@code RenderMode}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}.
781      * This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this
782      * mode can be animated and transformed, as well as positioned in the z-index between 2+ other
783      * Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture}
784      * are required, developers should strongly prefer the {@link RenderMode#surface} render mode.
785      */
786     texture
787   }
788 
789   /**
790    * Transparency mode for a {@code FlutterView}.
791    * <p>
792    * {@code TransparencyMode} impacts the visual behavior and performance of a {@link FlutterSurfaceView},
793    * which is displayed when a {@code FlutterView} uses {@link RenderMode#surface}.
794    * <p>
795    * {@code TransparencyMode} does not impact {@link FlutterTextureView}, which is displayed when
796    * a {@code FlutterView} uses {@link RenderMode#texture}, because a {@link FlutterTextureView}
797    * automatically comes with transparency.
798    */
799   public enum TransparencyMode {
800     /**
801      * Renders a {@code FlutterView} without any transparency. This affects {@code FlutterView}s in
802      * {@link RenderMode#surface} by introducing a base color of black, and places the
803      * {@link FlutterSurfaceView}'s {@code Window} behind all other content.
804      * <p>
805      * In {@link RenderMode#surface}, this mode is the most performant and is a good choice for
806      * fullscreen Flutter UIs that will not undergo {@code Fragment} transactions. If this mode is
807      * used within a {@code Fragment}, and that {@code Fragment} is replaced by another one, a
808      * brief black flicker may be visible during the switch.
809      */
810     opaque,
811     /**
812      * Renders a {@code FlutterView} with transparency. This affects {@code FlutterView}s in
813      * {@link RenderMode#surface} by allowing background transparency, and places the
814      * {@link FlutterSurfaceView}'s {@code Window} on top of all other content.
815      * <p>
816      * In {@link RenderMode#surface}, this mode is less performant than {@link #opaque}, but this
817      * mode avoids the black flicker problem that {@link #opaque} has when going through
818      * {@code Fragment} transactions. Consider using this {@code TransparencyMode} if you intend to
819      * switch {@code Fragment}s at runtime that contain a Flutter UI.
820      */
821     transparent
822   }
823 
824   /**
825    * Listener that is notified when a {@link FlutterEngine} is attached to/detached from
826    * a given {@code FlutterView}.
827    */
828   @VisibleForTesting
829   public interface FlutterEngineAttachmentListener {
830     /**
831      * The given {@code engine} has been attached to the associated {@code FlutterView}.
832      */
onFlutterEngineAttachedToFlutterView(@onNull FlutterEngine engine)833     void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine);
834 
835     /**
836      * A previously attached {@link FlutterEngine} has been detached from the associated
837      * {@code FlutterView}.
838      */
onFlutterEngineDetachedFromFlutterView()839     void onFlutterEngineDetachedFromFlutterView();
840   }
841 }
842