| // 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.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). | |
| * | |
| * <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}. | |
| */ | |
| FlutterView getFlutterView(); | |
| } | |
| private static final String TAG = "FlutterView"; | |
| static final class ViewportMetrics { | |
| float devicePixelRatio = 1.0f; | |
| int physicalWidth = 0; | |
| int physicalHeight = 0; | |
| int physicalPaddingTop = 0; | |
| int physicalPaddingRight = 0; | |
| int physicalPaddingBottom = 0; | |
| int physicalPaddingLeft = 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<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 = 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 onKeyUp(int keyCode, KeyEvent event) { | |
| if (!isAttached()) { | |
| return super.onKeyUp(keyCode, event); | |
| } | |
| return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event); | |
| } | |
| @Override | |
| public boolean onKeyDown(int keyCode, KeyEvent event) { | |
| if (!isAttached()) { | |
| return super.onKeyDown(keyCode, event); | |
| } | |
| return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, 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}. | |
| * | |
| * <p>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<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.physicalPaddingTop = uiInsets.top; | |
| mMetrics.physicalPaddingRight = uiInsets.right; | |
| mMetrics.physicalPaddingBottom = uiInsets.bottom; | |
| mMetrics.physicalPaddingLeft = 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.physicalPaddingTop = | |
| Math.max( | |
| Math.max(mMetrics.physicalPaddingTop, waterfallInsets.top), | |
| cutout.getSafeInsetTop()); | |
| mMetrics.physicalPaddingRight = | |
| Math.max( | |
| Math.max(mMetrics.physicalPaddingRight, waterfallInsets.right), | |
| cutout.getSafeInsetRight()); | |
| mMetrics.physicalPaddingBottom = | |
| Math.max( | |
| Math.max(mMetrics.physicalPaddingBottom, waterfallInsets.bottom), | |
| cutout.getSafeInsetBottom()); | |
| mMetrics.physicalPaddingLeft = | |
| Math.max( | |
| Math.max(mMetrics.physicalPaddingLeft, 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) and left/right system insets should partially obscure the content | |
| // (padding). | |
| mMetrics.physicalPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0; | |
| mMetrics.physicalPaddingRight = | |
| zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH | |
| ? 0 | |
| : insets.getSystemWindowInsetRight(); | |
| mMetrics.physicalPaddingBottom = 0; | |
| mMetrics.physicalPaddingLeft = | |
| 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 = | |
| navigationBarVisible | |
| ? insets.getSystemWindowInsetBottom() | |
| : 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.physicalPaddingTop = insets.top; | |
| mMetrics.physicalPaddingRight = insets.right; | |
| mMetrics.physicalPaddingBottom = 0; | |
| mMetrics.physicalPaddingLeft = 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.physicalPaddingTop, | |
| mMetrics.physicalPaddingRight, | |
| mMetrics.physicalPaddingBottom, | |
| mMetrics.physicalPaddingLeft, | |
| 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<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; | |
| } | |
| } | |
| @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(), surfaceTexture); | |
| return entry; | |
| } | |
| final class SurfaceTextureRegistryEntry implements TextureRegistry.SurfaceTextureEntry { | |
| private final long id; | |
| private final SurfaceTexture surfaceTexture; | |
| private boolean released; | |
| SurfaceTextureRegistryEntry(long id, SurfaceTexture surfaceTexture) { | |
| this.id = id; | |
| this.surfaceTexture = 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); | |
| } | |
| }; | |
| @Override | |
| public SurfaceTexture surfaceTexture() { | |
| return 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); | |
| surfaceTexture.release(); | |
| mNativeView.getFlutterJNI().unregisterTexture(id); | |
| } | |
| } | |
| } |