// 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.ContextWrapper; 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.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.AndroidKeyProcessor; import io.flutter.embedding.android.AndroidTouchProcessor; 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.KeyEventChannel; 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 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 { /** * Interface for those objects that maintain and expose a reference to a {@code FlutterView} (such * as a full-screen Flutter activity). * *

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}. */ 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; } private final DartExecutor dartExecutor; private final FlutterRenderer flutterRenderer; private final NavigationChannel navigationChannel; private final KeyEventChannel keyEventChannel; 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 AndroidKeyProcessor androidKeyProcessor; private final AndroidTouchProcessor androidTouchProcessor; private AccessibilityBridge mAccessibilityNodeProvider; private final SurfaceHolder.Callback mSurfaceCallback; private final ViewportMetrics mMetrics; private final List mActivityLifecycleListeners; private final List 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 = 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; 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); keyEventChannel = new KeyEventChannel(dartExecutor); lifecycleChannel = new LifecycleChannel(dartExecutor); localizationChannel = new LocalizationChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); systemChannel = new SystemChannel(dartExecutor); settingsChannel = new SettingsChannel(dartExecutor); // Create and setup 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); } else { mMouseCursorPlugin = null; } mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel); androidKeyProcessor = new AndroidKeyProcessor(this, keyEventChannel, mTextInputPlugin); 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(); } private static Activity getActivity(Context context) { if (context == null) { return null; } if (context instanceof Activity) { return (Activity) context; } if (context instanceof ContextWrapper) { // Recurse up chain of base contexts until we find an Activity. return getActivity(((ContextWrapper) context).getBaseContext()); } return null; } @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() && androidKeyProcessor.onKeyEvent(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); } /** * Updates this to support rendering as a transparent {@link SurfaceView}. * *

Sets it on top of its window. The background color still needs to be controlled from within * the Flutter UI itself. * * @deprecated FlutterView in the v1 embedding is always a SurfaceView and will cover * accessibility highlights when transparent. Consider migrating to the v2 Android embedding, * using {@link io.flutter.embedding.android.FlutterView.RenderMode#texture}, and setting * {@link io.flutter.embedding.android.FlutterView.TransparencyMode#transparent}. See also * https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects. */ @Deprecated public void enableTransparentBackground() { Log.w( TAG, "FlutterView in the v1 embedding is always a SurfaceView and will cover accessibility highlights when transparent. Consider migrating to the v2 Android embedding. https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects"); setZOrderOnTop(true); getHolder().setFormat(PixelFormat.TRANSPARENT); } /** * 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, 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 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. * *

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); } // Called by FlutterNativeView to notify first Flutter frame rendered. public void onFirstFrame() { didRenderFirstFrame = true; // Allow listeners to remove themselves when they are called. List 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; } } @Override @TargetApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N) @NonNull public PointerIcon getSystemPointerIcon(int type) { return PointerIcon.getSystemIcon(getContext(), type); } @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(String channel, BinaryMessageHandler handler) { mNativeView.setMessageHandler(channel, handler); } /** Listener will be called on the Android UI thread once when Flutter renders the first frame. */ public interface FirstFrameListener { void onFirstFrame(); } @Override public TextureRegistry.SurfaceTextureEntry createSurfaceTexture() { final SurfaceTexture surfaceTexture = new SurfaceTexture(0); 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); } } }