blob: eb498a2335e96b5973bf05c1c3ab6ef07db5845f [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.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Handler;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.DisplayCutout;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewConfiguration;
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.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import io.flutter.Log;
import io.flutter.app.FlutterPluginRegistry;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.mouse.MouseCursorPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.util.ViewUtils;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* Deprecated Android view containing a Flutter app.
*
* @deprecated {@link io.flutter.embedding.android.FlutterView} is the new API that now replaces
* this class. See https://flutter.dev/go/android-project-migration for more migration details.
*/
@Deprecated
public class FlutterView extends SurfaceView
implements BinaryMessenger,
TextureRegistry,
MouseCursorPlugin.MouseCursorViewDelegate,
KeyboardManager.ViewDelegate {
/**
* Interface for those objects that maintain and expose a reference to a {@code FlutterView} (such
* as a full-screen Flutter activity).
*
* <p>This indirection is provided to support applications that use an activity other than {@link
* io.flutter.app.FlutterActivity} (e.g. Android v4 support library's {@code FragmentActivity}).
* It allows Flutter plugins to deal in this interface and not require that the activity be a
* subclass of {@code FlutterActivity}.
*/
public interface Provider {
/**
* Returns a reference to the Flutter view maintained by this object. This may be {@code null}.
*
* @return a reference to the Flutter view maintained by this object.
*/
FlutterView getFlutterView();
}
private static final String TAG = "FlutterView";
static final class ViewportMetrics {
float devicePixelRatio = 1.0f;
int physicalWidth = 0;
int physicalHeight = 0;
int physicalViewPaddingTop = 0;
int physicalViewPaddingRight = 0;
int physicalViewPaddingBottom = 0;
int physicalViewPaddingLeft = 0;
int physicalViewInsetTop = 0;
int physicalViewInsetRight = 0;
int physicalViewInsetBottom = 0;
int physicalViewInsetLeft = 0;
int systemGestureInsetTop = 0;
int systemGestureInsetRight = 0;
int systemGestureInsetBottom = 0;
int systemGestureInsetLeft = 0;
int physicalTouchSlop = -1;
}
private final DartExecutor dartExecutor;
private final FlutterRenderer flutterRenderer;
private final NavigationChannel navigationChannel;
private final LifecycleChannel lifecycleChannel;
private final LocalizationChannel localizationChannel;
private final PlatformChannel platformChannel;
private final SettingsChannel settingsChannel;
private final SystemChannel systemChannel;
private final InputMethodManager mImm;
private final TextInputPlugin mTextInputPlugin;
private final LocalizationPlugin mLocalizationPlugin;
private final MouseCursorPlugin mMouseCursorPlugin;
private final KeyboardManager mKeyboardManager;
private final AndroidTouchProcessor androidTouchProcessor;
private AccessibilityBridge mAccessibilityNodeProvider;
private final SurfaceHolder.Callback mSurfaceCallback;
private final ViewportMetrics mMetrics;
private final List<ActivityLifecycleListener> mActivityLifecycleListeners;
private final List<FirstFrameListener> mFirstFrameListeners;
private final AtomicLong nextTextureId = new AtomicLong(0L);
private FlutterNativeView mNativeView;
private boolean mIsSoftwareRenderingEnabled = false; // using the software renderer or not
private boolean didRenderFirstFrame = false;
private final AccessibilityBridge.OnAccessibilityChangeListener onAccessibilityChangeListener =
new AccessibilityBridge.OnAccessibilityChangeListener() {
@Override
public void onAccessibilityChanged(
boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
resetWillNotDraw(isAccessibilityEnabled, isTouchExplorationEnabled);
}
};
public FlutterView(Context context) {
this(context, null);
}
public FlutterView(Context context, AttributeSet attrs) {
this(context, attrs, null);
}
public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) {
super(context, attrs);
Activity activity = ViewUtils.getActivity(getContext());
if (activity == null) {
throw new IllegalArgumentException("Bad context");
}
if (nativeView == null) {
mNativeView = new FlutterNativeView(activity.getApplicationContext());
} else {
mNativeView = nativeView;
}
dartExecutor = mNativeView.getDartExecutor();
flutterRenderer = new FlutterRenderer(mNativeView.getFlutterJNI());
mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().getIsSoftwareRenderingEnabled();
mMetrics = new ViewportMetrics();
mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
mMetrics.physicalTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
setFocusable(true);
setFocusableInTouchMode(true);
mNativeView.attachViewAndActivity(this, activity);
mSurfaceCallback =
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
assertAttached();
mNativeView.getFlutterJNI().onSurfaceCreated(holder.getSurface());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
assertAttached();
mNativeView.getFlutterJNI().onSurfaceChanged(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
assertAttached();
mNativeView.getFlutterJNI().onSurfaceDestroyed();
}
};
getHolder().addCallback(mSurfaceCallback);
mActivityLifecycleListeners = new ArrayList<>();
mFirstFrameListeners = new ArrayList<>();
// Create all platform channels
navigationChannel = new NavigationChannel(dartExecutor);
lifecycleChannel = new LifecycleChannel(dartExecutor);
localizationChannel = new LocalizationChannel(dartExecutor);
platformChannel = new PlatformChannel(dartExecutor);
systemChannel = new SystemChannel(dartExecutor);
settingsChannel = new SettingsChannel(dartExecutor);
// Create and set up plugins
PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel);
addActivityLifecycleListener(
new ActivityLifecycleListener() {
@Override
public void onPostResume() {
platformPlugin.updateSystemUiOverlays();
}
});
mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
PlatformViewsController platformViewsController =
mNativeView.getPluginRegistry().getPlatformViewsController();
mTextInputPlugin =
new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController);
mKeyboardManager = new KeyboardManager(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor));
} else {
mMouseCursorPlugin = null;
}
mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel);
androidTouchProcessor =
new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false);
platformViewsController.attachToFlutterRenderer(flutterRenderer);
mNativeView
.getPluginRegistry()
.getPlatformViewsController()
.attachTextInputPlugin(mTextInputPlugin);
mNativeView.getFlutterJNI().setLocalizationPlugin(mLocalizationPlugin);
// Send initial platform information to Dart
mLocalizationPlugin.sendLocalesToFlutter(getResources().getConfiguration());
sendUserPlatformSettingsToDart();
}
@NonNull
public DartExecutor getDartExecutor() {
return dartExecutor;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
Log.e(TAG, "dispatchKeyEvent: " + event.toString());
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
// Tell Android to start tracking this event.
getKeyDispatcherState().startTracking(event, this);
} else if (event.getAction() == KeyEvent.ACTION_UP) {
// Stop tracking the event.
getKeyDispatcherState().handleUpEvent(event);
}
// If the key processor doesn't handle it, then send it on to the
// superclass. The key processor will typically handle all events except
// those where it has re-dispatched the event after receiving a reply from
// the framework that the framework did not handle it.
return (isAttached() && mKeyboardManager.handleEvent(event)) || super.dispatchKeyEvent(event);
}
public FlutterNativeView getFlutterNativeView() {
return mNativeView;
}
public FlutterPluginRegistry getPluginRegistry() {
return mNativeView.getPluginRegistry();
}
public String getLookupKeyForAsset(String asset) {
return FlutterMain.getLookupKeyForAsset(asset);
}
public String getLookupKeyForAsset(String asset, String packageName) {
return FlutterMain.getLookupKeyForAsset(asset, packageName);
}
public void addActivityLifecycleListener(ActivityLifecycleListener listener) {
mActivityLifecycleListeners.add(listener);
}
public void onStart() {
lifecycleChannel.appIsInactive();
}
public void onPause() {
lifecycleChannel.appIsInactive();
}
public void onPostResume() {
for (ActivityLifecycleListener listener : mActivityLifecycleListeners) {
listener.onPostResume();
}
lifecycleChannel.appIsResumed();
}
public void onStop() {
lifecycleChannel.appIsPaused();
}
public void onMemoryPressure() {
mNativeView.getFlutterJNI().notifyLowMemoryWarning();
systemChannel.sendMemoryPressureWarning();
}
/**
* Returns true if the Flutter experience associated with this {@code FlutterView} has rendered
* its first frame, or false otherwise.
*/
public boolean hasRenderedFirstFrame() {
return didRenderFirstFrame;
}
/**
* Provide a listener that will be called once when the FlutterView renders its first frame to the
* underlaying SurfaceView.
*/
public void addFirstFrameListener(FirstFrameListener listener) {
mFirstFrameListeners.add(listener);
}
/** Remove an existing first frame listener. */
public void removeFirstFrameListener(FirstFrameListener listener) {
mFirstFrameListeners.remove(listener);
}
@Override
public void enableBufferingIncomingMessages() {}
@Override
public void disableBufferingIncomingMessages() {}
/**
* Reverts this back to the {@link SurfaceView} defaults, at the back of its window and opaque.
*/
public void disableTransparentBackground() {
setZOrderOnTop(false);
getHolder().setFormat(PixelFormat.OPAQUE);
}
public void setInitialRoute(String route) {
navigationChannel.setInitialRoute(route);
}
public void pushRoute(String route) {
navigationChannel.pushRoute(route);
}
public void popRoute() {
navigationChannel.popRoute();
}
private void sendUserPlatformSettingsToDart() {
// 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;
settingsChannel
.startMessage()
.setTextScaleFactor(getResources().getConfiguration().fontScale)
.setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
.setPlatformBrightness(brightness)
.send();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mLocalizationPlugin.sendLocalesToFlutter(newConfig);
sendUserPlatformSettingsToDart();
}
float getDevicePixelRatio() {
return mMetrics.devicePixelRatio;
}
public FlutterNativeView detach() {
if (!isAttached()) return null;
getHolder().removeCallback(mSurfaceCallback);
mNativeView.detachFromFlutterView();
FlutterNativeView view = mNativeView;
mNativeView = null;
return view;
}
public void destroy() {
if (!isAttached()) return;
getHolder().removeCallback(mSurfaceCallback);
releaseAccessibilityNodeProvider();
mNativeView.destroy();
mNativeView = null;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs);
}
@Override
public boolean checkInputConnectionProxy(View view) {
return mNativeView
.getPluginRegistry()
.getPlatformViewsController()
.checkInputConnectionProxy(view);
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);
mTextInputPlugin.onProvideAutofillVirtualStructure(structure, flags);
}
@Override
public void autofill(SparseArray<AutofillValue> values) {
mTextInputPlugin.autofill(values);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isAttached()) {
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);
}
@Override
public boolean onHoverEvent(MotionEvent event) {
if (!isAttached()) {
return super.onHoverEvent(event);
}
boolean handled = mAccessibilityNodeProvider.onAccessibilityHoverEvent(event);
if (!handled) {
// TODO(ianh): Expose hover events to the platform,
// implementing ADD, REMOVE, etc.
}
return handled;
}
/**
* 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(MotionEvent event) {
boolean handled = isAttached() && androidTouchProcessor.onGenericMotionEvent(event);
return handled ? true : super.onGenericMotionEvent(event);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
mMetrics.physicalWidth = width;
mMetrics.physicalHeight = height;
updateViewportMetrics();
super.onSizeChanged(width, height, oldWidth, oldHeight);
}
// TODO(garyq): Add support for notch cutout API
// 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();
}
}
// 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.
// 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.
@Override
@TargetApi(20)
@RequiresApi(20)
@SuppressLint({"InlinedApi", "NewApi"})
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
// getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;
}
boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
boolean navigationBarVisible =
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
int mask = 0;
if (navigationBarVisible) {
mask = mask | android.view.WindowInsets.Type.navigationBars();
}
if (statusBarVisible) {
mask = mask | android.view.WindowInsets.Type.statusBars();
}
Insets uiInsets = insets.getInsets(mask);
mMetrics.physicalViewPaddingTop = uiInsets.top;
mMetrics.physicalViewPaddingRight = uiInsets.right;
mMetrics.physicalViewPaddingBottom = uiInsets.bottom;
mMetrics.physicalViewPaddingLeft = uiInsets.left;
Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
mMetrics.physicalViewInsetTop = imeInsets.top;
mMetrics.physicalViewInsetRight = imeInsets.right;
mMetrics.physicalViewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
mMetrics.physicalViewInsetLeft = imeInsets.left;
Insets systemGestureInsets =
insets.getInsets(android.view.WindowInsets.Type.systemGestures());
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;
// TODO(garyq): Expose the full rects of the display cutout.
// Take the max of the display cutout insets and existing padding to merge them
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
Insets waterfallInsets = cutout.getWaterfallInsets();
mMetrics.physicalViewPaddingTop =
Math.max(
Math.max(mMetrics.physicalViewPaddingTop, waterfallInsets.top),
cutout.getSafeInsetTop());
mMetrics.physicalViewPaddingRight =
Math.max(
Math.max(mMetrics.physicalViewPaddingRight, waterfallInsets.right),
cutout.getSafeInsetRight());
mMetrics.physicalViewPaddingBottom =
Math.max(
Math.max(mMetrics.physicalViewPaddingBottom, waterfallInsets.bottom),
cutout.getSafeInsetBottom());
mMetrics.physicalViewPaddingLeft =
Math.max(
Math.max(mMetrics.physicalViewPaddingLeft, waterfallInsets.left),
cutout.getSafeInsetLeft());
}
} else {
// We zero the left and/or right sides to prevent the padding the
// navigation bar would have caused.
ZeroSides zeroSides = ZeroSides.NONE;
if (!navigationBarVisible) {
zeroSides = calculateShouldZeroSides();
}
// Status bar (top), navigation bar (bottom) and left/right system insets should
// partially obscure the content (padding).
mMetrics.physicalViewPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0;
mMetrics.physicalViewPaddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
mMetrics.physicalViewPaddingBottom =
navigationBarVisible && guessBottomKeyboardInset(insets) == 0
? insets.getSystemWindowInsetBottom()
: 0;
mMetrics.physicalViewPaddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
mMetrics.physicalViewInsetTop = 0;
mMetrics.physicalViewInsetRight = 0;
mMetrics.physicalViewInsetBottom = guessBottomKeyboardInset(insets);
mMetrics.physicalViewInsetLeft = 0;
}
updateViewportMetrics();
return super.onApplyWindowInsets(insets);
}
@Override
@SuppressWarnings("deprecation")
protected boolean fitSystemWindows(Rect insets) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// Status bar, left/right system insets partially obscure content (padding).
mMetrics.physicalViewPaddingTop = insets.top;
mMetrics.physicalViewPaddingRight = insets.right;
mMetrics.physicalViewPaddingBottom = 0;
mMetrics.physicalViewPaddingLeft = insets.left;
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
mMetrics.physicalViewInsetTop = 0;
mMetrics.physicalViewInsetRight = 0;
mMetrics.physicalViewInsetBottom = insets.bottom;
mMetrics.physicalViewInsetLeft = 0;
updateViewportMetrics();
return true;
} else {
return super.fitSystemWindows(insets);
}
}
private boolean isAttached() {
return mNativeView != null && mNativeView.isAttached();
}
void assertAttached() {
if (!isAttached()) throw new AssertionError("Platform view is not attached");
}
private void preRun() {
resetAccessibilityTree();
}
void resetAccessibilityTree() {
if (mAccessibilityNodeProvider != null) {
mAccessibilityNodeProvider.reset();
}
}
private void postRun() {}
public void runFromBundle(FlutterRunArguments args) {
assertAttached();
preRun();
mNativeView.runFromBundle(args);
postRun();
}
/**
* Return the most recent frame as a bitmap.
*
* @return A bitmap.
*/
public Bitmap getBitmap() {
assertAttached();
return mNativeView.getFlutterJNI().getBitmap();
}
private void updateViewportMetrics() {
if (!isAttached()) return;
mNativeView
.getFlutterJNI()
.setViewportMetrics(
mMetrics.devicePixelRatio,
mMetrics.physicalWidth,
mMetrics.physicalHeight,
mMetrics.physicalViewPaddingTop,
mMetrics.physicalViewPaddingRight,
mMetrics.physicalViewPaddingBottom,
mMetrics.physicalViewPaddingLeft,
mMetrics.physicalViewInsetTop,
mMetrics.physicalViewInsetRight,
mMetrics.physicalViewInsetBottom,
mMetrics.physicalViewInsetLeft,
mMetrics.systemGestureInsetTop,
mMetrics.systemGestureInsetRight,
mMetrics.systemGestureInsetBottom,
mMetrics.systemGestureInsetLeft,
mMetrics.physicalTouchSlop,
new int[0],
new int[0],
new int[0]);
}
// Called by FlutterNativeView to notify first Flutter frame rendered.
public void onFirstFrame() {
didRenderFirstFrame = true;
// Allow listeners to remove themselves when they are called.
List<FirstFrameListener> listeners = new ArrayList<>(mFirstFrameListeners);
for (FirstFrameListener listener : listeners) {
listener.onFirstFrame();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
PlatformViewsController platformViewsController =
getPluginRegistry().getPlatformViewsController();
mAccessibilityNodeProvider =
new AccessibilityBridge(
this,
new AccessibilityChannel(dartExecutor, getFlutterNativeView().getFlutterJNI()),
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
getContext().getContentResolver(),
platformViewsController);
mAccessibilityNodeProvider.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
resetWillNotDraw(
mAccessibilityNodeProvider.isAccessibilityEnabled(),
mAccessibilityNodeProvider.isTouchExplorationEnabled());
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
releaseAccessibilityNodeProvider();
}
// 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 (!mIsSoftwareRenderingEnabled) {
setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
} else {
setWillNotDraw(false);
}
}
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (mAccessibilityNodeProvider != null && mAccessibilityNodeProvider.isAccessibilityEnabled()) {
return mAccessibilityNodeProvider;
} else {
// TODO(goderbauer): when a11y is off this should return a one-off snapshot of
// the a11y
// tree.
return null;
}
}
private void releaseAccessibilityNodeProvider() {
if (mAccessibilityNodeProvider != null) {
mAccessibilityNodeProvider.release();
mAccessibilityNodeProvider = null;
}
}
// -------- Start: Mouse -------
@Override
@TargetApi(Build.VERSION_CODES.N)
@RequiresApi(Build.VERSION_CODES.N)
@NonNull
public PointerIcon getSystemPointerIcon(int type) {
return PointerIcon.getSystemIcon(getContext(), type);
}
// -------- End: Mouse -------
// -------- Start: Keyboard -------
@Override
public BinaryMessenger getBinaryMessenger() {
return this;
}
@Override
public boolean onTextInputKeyEvent(@NonNull KeyEvent keyEvent) {
return mTextInputPlugin.handleKeyEvent(keyEvent);
}
@Override
public void redispatch(@NonNull KeyEvent keyEvent) {
getRootView().dispatchKeyEvent(keyEvent);
}
// -------- End: Keyboard -------
@Override
@UiThread
public TaskQueue makeBackgroundTaskQueue(TaskQueueOptions options) {
return null;
}
@Override
@UiThread
public void send(String channel, ByteBuffer message) {
send(channel, message, null);
}
@Override
@UiThread
public void send(String channel, ByteBuffer message, BinaryReply callback) {
if (!isAttached()) {
Log.d(TAG, "FlutterView.send called on a detached view, channel=" + channel);
return;
}
mNativeView.send(channel, message, callback);
}
@Override
@UiThread
public void setMessageHandler(@NonNull String channel, @NonNull BinaryMessageHandler handler) {
mNativeView.setMessageHandler(channel, handler);
}
@Override
@UiThread
public void setMessageHandler(
@NonNull String channel,
@NonNull BinaryMessageHandler handler,
@NonNull TaskQueue taskQueue) {
mNativeView.setMessageHandler(channel, handler, taskQueue);
}
/** Listener will be called on the Android UI thread once when Flutter renders the first frame. */
public interface FirstFrameListener {
void onFirstFrame();
}
@Override
@NonNull
public TextureRegistry.SurfaceTextureEntry createSurfaceTexture() {
final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
return registerSurfaceTexture(surfaceTexture);
}
@Override
@NonNull
public TextureRegistry.SurfaceTextureEntry registerSurfaceTexture(
@NonNull SurfaceTexture surfaceTexture) {
surfaceTexture.detachFromGLContext();
final SurfaceTextureRegistryEntry entry =
new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
mNativeView.getFlutterJNI().registerTexture(entry.id(), entry.textureWrapper());
return entry;
}
final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry {
private final long id;
private final SurfaceTextureWrapper textureWrapper;
private boolean released;
SurfaceTextureRegistryEntry(long id, SurfaceTexture surfaceTexture) {
this.id = id;
this.textureWrapper = new SurfaceTextureWrapper(surfaceTexture);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// The callback relies on being executed on the UI thread (unsynchronised read of
// mNativeView
// and also the engine code check for platform thread in
// Shell::OnPlatformViewMarkTextureFrameAvailable),
// so we explicitly pass a Handler for the current thread.
this.surfaceTexture().setOnFrameAvailableListener(onFrameListener, new Handler());
} else {
// Android documentation states that the listener can be called on an arbitrary thread.
// But in practice, versions of Android that predate the newer API will call the listener
// on the thread where the SurfaceTexture was constructed.
this.surfaceTexture().setOnFrameAvailableListener(onFrameListener);
}
}
private SurfaceTexture.OnFrameAvailableListener onFrameListener =
new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture texture) {
if (released || mNativeView == null) {
// Even though we make sure to unregister the callback before releasing, as of Android
// O
// SurfaceTexture has a data race when accessing the callback, so the callback may
// still be called by a stale reference after released==true and mNativeView==null.
return;
}
mNativeView
.getFlutterJNI()
.markTextureFrameAvailable(SurfaceTextureRegistryEntry.this.id);
}
};
public SurfaceTextureWrapper textureWrapper() {
return textureWrapper;
}
@Override
public SurfaceTexture surfaceTexture() {
return textureWrapper.surfaceTexture();
}
@Override
public long id() {
return id;
}
@Override
public void release() {
if (released) {
return;
}
released = true;
// The ordering of the next 3 calls is important:
// First we remove the frame listener, then we release the SurfaceTexture, and only after we
// unregister
// the texture which actually deletes the GL texture.
// Otherwise onFrameAvailableListener might be called after mNativeView==null
// (https://github.com/flutter/flutter/issues/20951). See also the check in onFrameAvailable.
surfaceTexture().setOnFrameAvailableListener(null);
textureWrapper.release();
mNativeView.getFlutterJNI().unregisterTexture(id);
}
}
}