blob: 7f8b7440eafe8cc9a2334a4c5e71a2da4f912013 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Build;
import android.os.LocaleList;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewStructure;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.embedding.engine.renderer.RenderSurface;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.view.AccessibilityBridge;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Displays a Flutter UI on an Android device.
*
* <p>A {@code FlutterView}'s UI is painted by a corresponding {@link FlutterEngine}.
*
* <p>A {@code FlutterView} can operate in 2 different {@link
* io.flutter.embedding.android.RenderMode}s:
*
* <ol>
* <li>{@link io.flutter.embedding.android.RenderMode#surface}, which paints a Flutter UI to a
* {@link android.view.SurfaceView}. This mode has the best performance, but a {@code
* FlutterView} in this mode cannot be positioned between 2 other Android {@code View}s in the
* z-index, nor can it be animated/transformed. Unless the special capabilities of a {@link
* android.graphics.SurfaceTexture} are required, developers should strongly prefer this
* render mode.
* <li>{@link io.flutter.embedding.android.RenderMode#texture}, which paints a Flutter UI to a
* {@link android.graphics.SurfaceTexture}. This mode is not as performant as {@link
* io.flutter.embedding.android.RenderMode#surface}, but a {@code FlutterView} in this mode
* can be animated and transformed, as well as positioned in the z-index between 2+ other
* Android {@code Views}. Unless the special capabilities of a {@link
* android.graphics.SurfaceTexture} are required, developers should strongly prefer the {@link
* io.flutter.embedding.android.RenderMode#surface} render mode.
* </ol>
*
* See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more
* information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}.
*/
public class FlutterView extends FrameLayout {
private static final String TAG = "FlutterView";
// Internal view hierarchy references.
@Nullable private FlutterSurfaceView flutterSurfaceView;
@Nullable private FlutterTextureView flutterTextureView;
@Nullable private RenderSurface renderSurface;
private final Set<FlutterUiDisplayListener> flutterUiDisplayListeners = new HashSet<>();
private boolean isFlutterUiDisplayed;
// Connections to a Flutter execution context.
@Nullable private FlutterEngine flutterEngine;
@NonNull
private final Set<FlutterEngineAttachmentListener> flutterEngineAttachmentListeners =
new HashSet<>();
// Components that process various types of Android View input and events,
// possibly storing intermediate state, and communicating those events to Flutter.
//
// These components essentially add some additional behavioral logic on top of
// existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc.
@Nullable private TextInputPlugin textInputPlugin;
@Nullable private AndroidKeyProcessor androidKeyProcessor;
@Nullable private AndroidTouchProcessor androidTouchProcessor;
@Nullable private AccessibilityBridge accessibilityBridge;
// Directly implemented View behavior that communicates with Flutter.
private final FlutterRenderer.ViewportMetrics viewportMetrics =
new FlutterRenderer.ViewportMetrics();
private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener =
new AccessibilityBridge.OnAccessibilityChangeListener() {
@Override
public void onAccessibilityChanged(
boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
}
};
private final FlutterUiDisplayListener flutterUiDisplayListener =
new FlutterUiDisplayListener() {
@Override
public void onFlutterUiDisplayed() {
isFlutterUiDisplayed = true;
for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
listener.onFlutterUiDisplayed();
}
}
@Override
public void onFlutterUiNoLongerDisplayed() {
isFlutterUiDisplayed = false;
for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
listener.onFlutterUiNoLongerDisplayed();
}
}
};
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes.
*
* <p>
*
* <ul>
* <li>A {@link FlutterSurfaceView} is used to render the Flutter UI.
* <li>{@code transparencyMode} defaults to {@link TransparencyMode#opaque}.
* </ul>
*
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
* compatible with {@link PlatformViewsController}.
*/
public FlutterView(@NonNull Context context) {
this(context, null, new FlutterSurfaceView(context));
}
/**
* Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
* #FlutterView(Context, FlutterTextureView)} instead.
*/
@Deprecated
public FlutterView(@NonNull Context context, @NonNull RenderMode renderMode) {
super(context, null);
if (renderMode == RenderMode.surface) {
flutterSurfaceView = new FlutterSurfaceView(context);
renderSurface = flutterSurfaceView;
} else {
flutterTextureView = new FlutterTextureView(context);
renderSurface = flutterTextureView;
}
init();
}
/**
* Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
* #FlutterView(Context, FlutterTextureView)} instead, and configure the incoming {@code
* FlutterSurfaceView} or {@code FlutterTextureView} for transparency as desired.
*
* <p>Constructs a {@code FlutterView} programmatically, without any XML attributes, uses a {@link
* FlutterSurfaceView} to render the Flutter UI, and allows selection of a {@code
* transparencyMode}.
*
* <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
* compatible with {@link PlatformViewsController}.
*/
@Deprecated
public FlutterView(@NonNull Context context, @NonNull TransparencyMode transparencyMode) {
this(
context,
null,
new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent));
}
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given
* {@link FlutterSurfaceView} to render the Flutter UI, and allows selection of a {@code
* transparencyMode}.
*
* <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
* compatible with {@link PlatformViewsController}.
*/
public FlutterView(@NonNull Context context, @NonNull FlutterSurfaceView flutterSurfaceView) {
this(context, null, flutterSurfaceView);
}
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes, uses the given
* {@link FlutterTextureView} to render the Flutter UI, and allows selection of a {@code
* transparencyMode}.
*
* <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
* compatible with {@link PlatformViewsController}.
*/
public FlutterView(@NonNull Context context, @NonNull FlutterTextureView flutterTextureView) {
this(context, null, flutterTextureView);
}
/**
* Constructs a {@code FlutterView} in an XML-inflation-compliant manner.
*
* <p>{@code FlutterView} requires an {@code Activity} instead of a generic {@code Context} to be
* compatible with {@link PlatformViewsController}.
*/
// TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
public FlutterView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, new FlutterSurfaceView(context));
}
/**
* Deprecated - use {@link #FlutterView(Context, FlutterSurfaceView)} or {@link
* #FlutterView(Context, FlutterTextureView)} instead, and configure the incoming {@code
* FlutterSurfaceView} or {@code FlutterTextureView} for transparency as desired.
*/
@Deprecated
public FlutterView(
@NonNull Context context,
@NonNull RenderMode renderMode,
@NonNull TransparencyMode transparencyMode) {
super(context, null);
if (renderMode == RenderMode.surface) {
flutterSurfaceView =
new FlutterSurfaceView(context, transparencyMode == TransparencyMode.transparent);
renderSurface = flutterSurfaceView;
} else {
flutterTextureView = new FlutterTextureView(context);
renderSurface = flutterTextureView;
}
init();
}
private FlutterView(
@NonNull Context context,
@Nullable AttributeSet attrs,
@NonNull FlutterSurfaceView flutterSurfaceView) {
super(context, attrs);
this.flutterSurfaceView = flutterSurfaceView;
this.renderSurface = flutterSurfaceView;
init();
}
private FlutterView(
@NonNull Context context,
@Nullable AttributeSet attrs,
@NonNull FlutterTextureView flutterTextureView) {
super(context, attrs);
this.flutterTextureView = flutterTextureView;
this.renderSurface = flutterTextureView;
init();
}
private void init() {
Log.v(TAG, "Initializing FlutterView");
if (flutterSurfaceView != null) {
Log.v(TAG, "Internally using a FlutterSurfaceView.");
addView(flutterSurfaceView);
} else {
Log.v(TAG, "Internally using a FlutterTextureView.");
addView(flutterTextureView);
}
// FlutterView needs to be focusable so that the InputMethodManager can interact with it.
setFocusable(true);
setFocusableInTouchMode(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS);
}
}
/**
* Returns true if an attached {@link FlutterEngine} has rendered at least 1 frame to this {@code
* FlutterView}.
*
* <p>Returns false if no {@link FlutterEngine} is attached.
*
* <p>This flag is specific to a given {@link FlutterEngine}. The following hypothetical timeline
* demonstrates how this flag changes over time.
*
* <ol>
* <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false
* <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true
* <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false
* <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false
* <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true
* </ol>
*/
public boolean hasRenderedFirstFrame() {
return isFlutterUiDisplayed;
}
/**
* Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
* first rendered frame.
*/
public void addOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) {
flutterUiDisplayListeners.add(listener);
}
/**
* Removes the given {@code listener}, which was previously added with {@link
* #addOnFirstFrameRenderedListener(FlutterUiDisplayListener)}.
*/
public void removeOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) {
flutterUiDisplayListeners.remove(listener);
}
// ------- Start: Process View configuration that Flutter cares about. ------
/**
* Sends relevant configuration data from Android to Flutter when the Android {@link
* Configuration} changes.
*
* <p>The Android {@link Configuration} might change as a result of device orientation change,
* device language change, device text scale factor change, etc.
*/
@Override
protected void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// We've observed on Android Q that going to the background, changing
// orientation, and bringing the app back to foreground results in a sequence
// of detatch from flutterEngine, onConfigurationChanged, followed by attach
// to flutterEngine.
// No-op here so that we avoid NPE; these channels will get notified once
// the activity or fragment tell the view to attach to the Flutter engine
// again (e.g. in onStart).
if (flutterEngine != null) {
Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter.");
sendLocalesToFlutter(newConfig);
sendUserSettingsToFlutter();
}
}
/**
* Invoked when this {@code FlutterView} changes size, including upon initial measure.
*
* <p>The initial measure reports an {@code oldWidth} and {@code oldHeight} of zero.
*
* <p>Flutter cares about the width and height of the view that displays it on the host platform.
* Therefore, when this method is invoked, the new width and height are communicated to Flutter as
* the "physical size" of the view that displays Flutter's UI.
*/
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
Log.v(
TAG,
"Size changed. Sending Flutter new viewport metrics. FlutterView was "
+ oldWidth
+ " x "
+ oldHeight
+ ", it is now "
+ width
+ " x "
+ height);
viewportMetrics.width = width;
viewportMetrics.height = height;
sendViewportMetricsToFlutter();
}
// TODO(garyq): Add support for notch cutout API: https://github.com/flutter/flutter/issues/56592
// Decide if we want to zero the padding of the sides. When in Landscape orientation,
// android may decide to place the software navigation bars on the side. When the nav
// bar is hidden, the reported insets should be removed to prevent extra useless space
// on the sides.
private enum ZeroSides {
NONE,
LEFT,
RIGHT,
BOTH
}
private ZeroSides calculateShouldZeroSides() {
// We get both orientation and rotation because rotation is all 4
// rotations relative to default rotation while orientation is portrait
// or landscape. By combining both, we can obtain a more precise measure
// of the rotation.
Context context = getContext();
int orientation = context.getResources().getConfiguration().orientation;
int rotation =
((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRotation();
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
if (rotation == Surface.ROTATION_90) {
return ZeroSides.RIGHT;
} else if (rotation == Surface.ROTATION_270) {
// In android API >= 23, the nav bar always appears on the "bottom" (USB) side.
return Build.VERSION.SDK_INT >= 23 ? ZeroSides.LEFT : ZeroSides.RIGHT;
}
// Ambiguous orientation due to landscape left/right default. Zero both sides.
else if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
return ZeroSides.BOTH;
}
}
// Square orientation deprecated in API 16, we will not check for it and return false
// to be safe and not remove any unique padding for the devices that do use it.
return ZeroSides.NONE;
}
// TODO(garyq): Use new Android R getInsets API
// TODO(garyq): The keyboard detection may interact strangely with
// https://github.com/flutter/flutter/issues/22061
// Uses inset heights and screen heights as a heuristic to determine if the insets should
// be padded. When the on-screen keyboard is detected, we want to include the full inset
// but when the inset is just the hidden nav bar, we want to provide a zero inset so the space
// can be used.
@TargetApi(20)
@RequiresApi(20)
private int guessBottomKeyboardInset(WindowInsets insets) {
int screenHeight = getRootView().getHeight();
// Magic number due to this being a heuristic. This should be replaced, but we have not
// found a clean way to do it yet (Sept. 2018)
final double keyboardHeightRatioHeuristic = 0.18;
if (insets.getSystemWindowInsetBottom() < screenHeight * keyboardHeightRatioHeuristic) {
// Is not a keyboard, so return zero as inset.
return 0;
} else {
// Is a keyboard, so return the full inset.
return insets.getSystemWindowInsetBottom();
}
}
/**
* Invoked when Android's desired window insets change, i.e., padding.
*
* <p>Flutter does not use a standard {@code View} hierarchy and therefore Flutter is unaware of
* these insets. Therefore, this method calculates the viewport metrics that Flutter should use
* and then sends those metrics to Flutter.
*
* <p>This callback is not present in API < 20, which means lower API devices will see the wider
* than expected padding when the status and navigation bars are hidden.
*/
@Override
@TargetApi(20)
@RequiresApi(20)
// The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings
// caused by usage of Android Q APIs. These calls are safe because they are
// guarded.
@SuppressLint({"InlinedApi", "NewApi"})
@NonNull
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
WindowInsets newInsets = super.onApplyWindowInsets(insets);
boolean statusBarHidden = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) != 0;
boolean navigationBarHidden =
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) != 0;
// We zero the left and/or right sides to prevent the padding the
// navigation bar would have caused.
ZeroSides zeroSides = ZeroSides.NONE;
if (navigationBarHidden) {
zeroSides = calculateShouldZeroSides();
}
// Status bar (top) and left/right system insets should partially obscure the content (padding).
viewportMetrics.paddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop();
viewportMetrics.paddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
viewportMetrics.paddingBottom = 0;
viewportMetrics.paddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom =
navigationBarHidden
? guessBottomKeyboardInset(insets)
: insets.getSystemWindowInsetBottom();
viewportMetrics.viewInsetLeft = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
}
Log.v(
TAG,
"Updating window insets (onApplyWindowInsets()):\n"
+ "Status bar insets: Top: "
+ viewportMetrics.paddingTop
+ ", Left: "
+ viewportMetrics.paddingLeft
+ ", Right: "
+ viewportMetrics.paddingRight
+ "\n"
+ "Keyboard insets: Bottom: "
+ viewportMetrics.viewInsetBottom
+ ", Left: "
+ viewportMetrics.viewInsetLeft
+ ", Right: "
+ viewportMetrics.viewInsetRight
+ "System Gesture Insets - Left: "
+ viewportMetrics.systemGestureInsetLeft
+ ", Top: "
+ viewportMetrics.systemGestureInsetTop
+ ", Right: "
+ viewportMetrics.systemGestureInsetRight
+ ", Bottom: "
+ viewportMetrics.viewInsetBottom);
sendViewportMetricsToFlutter();
return newInsets;
}
/**
* Invoked when Android's desired window insets change, i.e., padding.
*
* <p>{@code fitSystemWindows} is an earlier version of {@link
* #onApplyWindowInsets(WindowInsets)}. See that method for more details about how window insets
* relate to Flutter.
*/
@Override
@SuppressWarnings("deprecation")
protected boolean fitSystemWindows(@NonNull Rect insets) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// Status bar, left/right system insets partially obscure content (padding).
viewportMetrics.paddingTop = insets.top;
viewportMetrics.paddingRight = insets.right;
viewportMetrics.paddingBottom = 0;
viewportMetrics.paddingLeft = insets.left;
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = insets.bottom;
viewportMetrics.viewInsetLeft = 0;
Log.v(
TAG,
"Updating window insets (fitSystemWindows()):\n"
+ "Status bar insets: Top: "
+ viewportMetrics.paddingTop
+ ", Left: "
+ viewportMetrics.paddingLeft
+ ", Right: "
+ viewportMetrics.paddingRight
+ "\n"
+ "Keyboard insets: Bottom: "
+ viewportMetrics.viewInsetBottom
+ ", Left: "
+ viewportMetrics.viewInsetLeft
+ ", Right: "
+ viewportMetrics.viewInsetRight);
sendViewportMetricsToFlutter();
return true;
} else {
return super.fitSystemWindows(insets);
}
}
// ------- End: Process View configuration that Flutter cares about. --------
// -------- Start: Process UI I/O that Flutter cares about. -------
/**
* Creates an {@link InputConnection} to work with a {@link
* android.view.inputmethod.InputMethodManager}.
*
* <p>Any {@code View} that can take focus or process text input must implement this method by
* returning a non-null {@code InputConnection}. Flutter may render one or many focusable and
* text-input widgets, therefore {@code FlutterView} must support an {@code InputConnection}.
*
* <p>The {@code InputConnection} returned from this method comes from a {@link TextInputPlugin},
* which is owned by this {@code FlutterView}. A {@link TextInputPlugin} exists to encapsulate the
* nuances of input communication, rather than spread that logic throughout this {@code
* FlutterView}.
*/
@Override
@Nullable
public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
if (!isAttachedToFlutterEngine()) {
return super.onCreateInputConnection(outAttrs);
}
return textInputPlugin.createInputConnection(this, outAttrs);
}
/**
* Allows a {@code View} that is not currently the input connection target to invoke commands on
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
*
* <p>Returns true to allow non-input-connection-targets to invoke methods on {@code
* InputMethodManager}, or false to exclusively allow the input connection target to invoke such
* methods.
*/
@Override
public boolean checkInputConnectionProxy(View view) {
return flutterEngine != null
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
: super.checkInputConnectionProxy(view);
}
/**
* Invoked when key is released.
*
* <p>This method is typically invoked in response to the release of a physical keyboard key or a
* D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
* software keyboard may choose to invoke this method in some situations.
*
* <p>{@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} may do some
* additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the
* previous {@code keyCode} to generate a unicode combined character.
*/
@Override
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onKeyUp(keyCode, event);
}
androidKeyProcessor.onKeyUp(event);
return super.onKeyUp(keyCode, event);
}
/**
* Invoked when key is pressed.
*
* <p>This method is typically invoked in response to the press of a physical keyboard key or a
* D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
* software keyboard may choose to invoke this method in some situations.
*
* <p>{@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} may do some
* additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the
* previous {@code keyCode} to generate a unicode combined character.
*/
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onKeyDown(keyCode, event);
}
androidKeyProcessor.onKeyDown(event);
return super.onKeyDown(keyCode, event);
}
/**
* Invoked by Android when a user touch event occurs.
*
* <p>Flutter handles all of its own gesture detection and processing, therefore this method
* forwards all {@link MotionEvent} data from Android to Flutter.
*/
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onTouchEvent(event);
}
// TODO(abarth): This version check might not be effective in some
// versions of Android that statically compile code and will be upset
// at the lack of |requestUnbufferedDispatch|. Instead, we should factor
// version-dependent code into separate classes for each supported
// version and dispatch dynamically.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
requestUnbufferedDispatch(event);
}
return androidTouchProcessor.onTouchEvent(event);
}
/**
* Invoked by Android when a generic motion event occurs, e.g., joystick movement, mouse hover,
* track pad touches, scroll wheel movements, etc.
*
* <p>Flutter handles all of its own gesture detection and processing, therefore this method
* forwards all {@link MotionEvent} data from Android to Flutter.
*/
@Override
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
boolean handled =
isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
return handled ? true : super.onGenericMotionEvent(event);
}
/**
* Invoked by Android when a hover-compliant input system causes a hover event.
*
* <p>An example of hover events is a stylus sitting near an Android screen. As the stylus moves
* from outside a {@code View} to hover over a {@code View}, or move around within a {@code View},
* or moves from over a {@code View} to outside a {@code View}, a corresponding {@link
* MotionEvent} is reported via this method.
*
* <p>Hover events can be used for accessibility touch exploration and therefore are processed
* here for accessibility purposes.
*/
@Override
public boolean onHoverEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onHoverEvent(event);
}
boolean handled = accessibilityBridge.onAccessibilityHoverEvent(event);
if (!handled) {
// TODO(ianh): Expose hover events to the platform,
// implementing ADD, REMOVE, etc.
}
return handled;
}
// -------- End: Process UI I/O that Flutter cares about. ---------
// -------- Start: Accessibility -------
@Override
@Nullable
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
return accessibilityBridge;
} else {
// TODO(goderbauer): when a11y is off this should return a one-off snapshot of
// the a11y
// tree.
return null;
}
}
// TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise
// add comments.
private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) {
setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
} else {
setWillNotDraw(false);
}
}
// -------- End: Accessibility ---------
/**
* Connects this {@code FlutterView} to the given {@link FlutterEngine}.
*
* <p>This {@code FlutterView} will begin rendering the UI painted by the given {@link
* FlutterEngine}. This {@code FlutterView} will also begin forwarding interaction events from
* this {@code FlutterView} to the given {@link FlutterEngine}, e.g., user touch events,
* accessibility events, keyboard events, and others.
*
* <p>See {@link #detachFromFlutterEngine()} for information on how to detach from a {@link
* FlutterEngine}.
*/
public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Log.v(TAG, "Attaching to a FlutterEngine: " + flutterEngine);
if (isAttachedToFlutterEngine()) {
if (flutterEngine == this.flutterEngine) {
// We are already attached to this FlutterEngine
Log.v(TAG, "Already attached to this engine. Doing nothing.");
return;
}
// Detach from a previous FlutterEngine so we can attach to this new one.
Log.v(
TAG,
"Currently attached to a different engine. Detaching and then attaching"
+ " to new engine.");
detachFromFlutterEngine();
}
this.flutterEngine = flutterEngine;
// Instruct our FlutterRenderer that we are now its designated RenderSurface.
FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
isFlutterUiDisplayed = flutterRenderer.isDisplayingFlutterUi();
renderSurface.attachToRenderer(flutterRenderer);
flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
// Initialize various components that know how to process Android View I/O
// in a way that Flutter understands.
textInputPlugin =
new TextInputPlugin(
this,
this.flutterEngine.getTextInputChannel(),
this.flutterEngine.getPlatformViewsController());
androidKeyProcessor =
new AndroidKeyProcessor(this.flutterEngine.getKeyEventChannel(), textInputPlugin);
androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
accessibilityBridge =
new AccessibilityBridge(
this,
flutterEngine.getAccessibilityChannel(),
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
getContext().getContentResolver(),
this.flutterEngine.getPlatformViewsController());
accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
resetWillNotDraw(
accessibilityBridge.isAccessibilityEnabled(),
accessibilityBridge.isTouchExplorationEnabled());
// Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine.
// This allows platform Views to hook into Flutter's overall accessibility system.
this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge);
// Inform the Android framework that it should retrieve a new InputConnection
// now that an engine is attached.
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
textInputPlugin.getInputMethodManager().restartInput(this);
// Push View and Context related information from Android to Flutter.
sendUserSettingsToFlutter();
sendLocalesToFlutter(getResources().getConfiguration());
sendViewportMetricsToFlutter();
flutterEngine.getPlatformViewsController().attachToView(this);
// Notify engine attachment listeners of the attachment.
for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
}
// If the first frame has already been rendered, notify all first frame listeners.
// Do this after all other initialization so that listeners don't inadvertently interact
// with a FlutterView that is only partially attached to a FlutterEngine.
if (isFlutterUiDisplayed) {
flutterUiDisplayListener.onFlutterUiDisplayed();
}
}
/**
* Disconnects this {@code FlutterView} from a previously attached {@link FlutterEngine}.
*
* <p>This {@code FlutterView} will clear its UI and stop forwarding all events to the
* previously-attached {@link FlutterEngine}. This includes touch events, accessibility events,
* keyboard events, and others.
*
* <p>See {@link #attachToFlutterEngine(FlutterEngine)} for information on how to attach a {@link
* FlutterEngine}.
*/
public void detachFromFlutterEngine() {
Log.v(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
if (!isAttachedToFlutterEngine()) {
Log.v(TAG, "Not attached to an engine. Doing nothing.");
return;
}
// Notify engine attachment listeners of the detachment.
for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineDetachedFromFlutterView();
}
flutterEngine.getPlatformViewsController().detachFromView();
// Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
flutterEngine.getPlatformViewsController().detachAccessibiltyBridge();
// Disconnect and clean up the AccessibilityBridge.
accessibilityBridge.release();
accessibilityBridge = null;
// Inform the Android framework that it should retrieve a new InputConnection
// now that the engine is detached. The new InputConnection will be null, which
// signifies that this View does not process input (until a new engine is attached).
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
textInputPlugin.getInputMethodManager().restartInput(this);
textInputPlugin.destroy();
// Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
isFlutterUiDisplayed = false;
flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
flutterRenderer.stopRenderingToSurface();
flutterRenderer.setSemanticsEnabled(false);
renderSurface.detachFromRenderer();
flutterEngine = null;
}
/** Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}. */
@VisibleForTesting
public boolean isAttachedToFlutterEngine() {
return flutterEngine != null
&& flutterEngine.getRenderer() == renderSurface.getAttachedRenderer();
}
/**
* Returns the {@link FlutterEngine} to which this {@code FlutterView} is currently attached, or
* null if this {@code FlutterView} is not currently attached to a {@link FlutterEngine}.
*/
@VisibleForTesting
@Nullable
public FlutterEngine getAttachedFlutterEngine() {
return flutterEngine;
}
/**
* Adds a {@link FlutterEngineAttachmentListener}, which is notifed whenever this {@code
* FlutterView} attached to/detaches from a {@link FlutterEngine}.
*/
@VisibleForTesting
public void addFlutterEngineAttachmentListener(
@NonNull FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.add(listener);
}
/**
* Removes a {@link FlutterEngineAttachmentListener} that was previously added with {@link
* #addFlutterEngineAttachmentListener(FlutterEngineAttachmentListener)}.
*/
@VisibleForTesting
public void removeFlutterEngineAttachmentListener(
@NonNull FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.remove(listener);
}
/**
* Send the current {@link Locale} configuration to Flutter.
*
* <p>FlutterEngine must be non-null when this method is invoked.
*/
@SuppressWarnings("deprecation")
private void sendLocalesToFlutter(@NonNull Configuration config) {
List<Locale> locales = new ArrayList<>();
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
LocaleList localeList = config.getLocales();
int localeCount = localeList.size();
for (int index = 0; index < localeCount; ++index) {
Locale locale = localeList.get(index);
locales.add(locale);
}
} else {
locales.add(config.locale);
}
Locale platformResolvedLocale = null;
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
List<Locale.LanguageRange> languageRanges = new ArrayList<>();
LocaleList localeList = config.getLocales();
int localeCount = localeList.size();
for (int index = 0; index < localeCount; ++index) {
Locale locale = localeList.get(index);
languageRanges.add(new Locale.LanguageRange(locale.toLanguageTag()));
}
// TODO(garyq) implement a real locale resolution.
platformResolvedLocale =
Locale.lookup(languageRanges, Arrays.asList(Locale.getAvailableLocales()));
}
flutterEngine.getLocalizationChannel().sendLocales(locales, platformResolvedLocale);
}
/**
* Send various user preferences of this Android device to Flutter.
*
* <p>For example, sends the user's "text scale factor" preferences, as well as the user's clock
* format preference.
*
* <p>FlutterEngine must be non-null when this method is invoked.
*/
@VisibleForTesting
/* package */ void sendUserSettingsToFlutter() {
// Lookup the current brightness of the Android OS.
boolean isNightModeOn =
(getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
SettingsChannel.PlatformBrightness brightness =
isNightModeOn
? SettingsChannel.PlatformBrightness.dark
: SettingsChannel.PlatformBrightness.light;
flutterEngine
.getSettingsChannel()
.startMessage()
.setTextScaleFactor(getResources().getConfiguration().fontScale)
.setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
.setPlatformBrightness(brightness)
.send();
}
// TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI
private void sendViewportMetricsToFlutter() {
if (!isAttachedToFlutterEngine()) {
Log.w(
TAG,
"Tried to send viewport metrics from Android to Flutter but this "
+ "FlutterView was not attached to a FlutterEngine.");
return;
}
viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density;
flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);
textInputPlugin.onProvideAutofillVirtualStructure(structure, flags);
}
@Override
public void autofill(SparseArray<AutofillValue> values) {
textInputPlugin.autofill(values);
}
/**
* Render modes for a {@link FlutterView}.
*
* <p>Deprecated - please use {@link io.flutter.embedding.android.RenderMode} instead.
*/
@Deprecated()
public enum RenderMode {
/**
* {@code RenderMode}, which paints a Flutter UI to a {@link android.view.SurfaceView}. This
* mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned
* between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed.
* Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required,
* developers should strongly prefer this render mode.
*/
surface,
/**
* {@code RenderMode}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}.
* This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in
* this mode can be animated and transformed, as well as positioned in the z-index between 2+
* other Android {@code Views}. Unless the special capabilities of a {@link
* android.graphics.SurfaceTexture} are required, developers should strongly prefer the {@link
* RenderMode#surface} render mode.
*/
texture
}
/**
* Transparency mode for a {@code FlutterView}.
*
* <p>Deprecated - please use {@link io.flutter.embedding.android.TransparencyMode} instead.
*
* <p>{@code TransparencyMode} impacts the visual behavior and performance of a {@link
* FlutterSurfaceView}, which is displayed when a {@code FlutterView} uses {@link
* RenderMode#surface}.
*
* <p>{@code TransparencyMode} does not impact {@link FlutterTextureView}, which is displayed when
* a {@code FlutterView} uses {@link RenderMode#texture}, because a {@link FlutterTextureView}
* automatically comes with transparency.
*/
@Deprecated
public enum TransparencyMode {
/**
* Renders a {@code FlutterView} without any transparency. This affects {@code FlutterView}s in
* {@link io.flutter.embedding.android.RenderMode#surface} by introducing a base color of black,
* and places the {@link FlutterSurfaceView}'s {@code Window} behind all other content.
*
* <p>In {@link io.flutter.embedding.android.RenderMode#surface}, this mode is the most
* performant and is a good choice for fullscreen Flutter UIs that will not undergo {@code
* Fragment} transactions. If this mode is used within a {@code Fragment}, and that {@code
* Fragment} is replaced by another one, a brief black flicker may be visible during the switch.
*/
opaque,
/**
* Renders a {@code FlutterView} with transparency. This affects {@code FlutterView}s in {@link
* io.flutter.embedding.android.RenderMode#surface} by allowing background transparency, and
* places the {@link FlutterSurfaceView}'s {@code Window} on top of all other content.
*
* <p>In {@link io.flutter.embedding.android.RenderMode#surface}, this mode is less performant
* than {@link #opaque}, but this mode avoids the black flicker problem that {@link #opaque} has
* when going through {@code Fragment} transactions. Consider using this {@code
* TransparencyMode} if you intend to switch {@code Fragment}s at runtime that contain a Flutter
* UI.
*/
transparent
}
/**
* Listener that is notified when a {@link FlutterEngine} is attached to/detached from a given
* {@code FlutterView}.
*/
@VisibleForTesting
public interface FlutterEngineAttachmentListener {
/** The given {@code engine} has been attached to the associated {@code FlutterView}. */
void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine);
/**
* A previously attached {@link FlutterEngine} has been detached from the associated {@code
* FlutterView}.
*/
void onFlutterEngineDetachedFromFlutterView();
}
}