// 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.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.LocaleList; import android.provider.Settings; import android.text.format.DateFormat; import android.util.AttributeSet; import android.util.Log; import android.view.*; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import io.flutter.app.FlutterPluginRegistry; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.android.AndroidKeyProcessor; import io.flutter.embedding.engine.dart.DartExecutor; 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.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.plugin.common.*; import io.flutter.plugin.editing.TextInputPlugin; import io.flutter.plugin.platform.PlatformPlugin; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; import java.util.concurrent.atomic.AtomicLong; /** * An Android view containing a Flutter app. */ public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry, AccessibilityManager.AccessibilityStateChangeListener { /** * 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 physicalPaddingTop = 0; int physicalPaddingRight = 0; int physicalPaddingBottom = 0; int physicalPaddingLeft = 0; int physicalViewInsetTop = 0; int physicalViewInsetRight = 0; int physicalViewInsetBottom = 0; int physicalViewInsetLeft = 0; } private final DartExecutor dartExecutor; private final AccessibilityChannel accessibilityChannel; 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 AndroidKeyProcessor androidKeyProcessor; private AccessibilityBridge mAccessibilityNodeProvider; private final SurfaceHolder.Callback mSurfaceCallback; private final ViewportMetrics mMetrics; private final AccessibilityManager mAccessibilityManager; private final List mActivityLifecycleListeners; private final List mFirstFrameListeners; private final AtomicLong nextTextureId = new AtomicLong(0L); private FlutterNativeView mNativeView; private final AnimationScaleObserver mAnimationScaleObserver; private boolean mIsSoftwareRenderingEnabled = false; // using the software renderer or not 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 = (Activity) getContext(); if (nativeView == null) { mNativeView = new FlutterNativeView(activity.getApplicationContext()); } else { mNativeView = nativeView; } dartExecutor = mNativeView.getDartExecutor(); mIsSoftwareRenderingEnabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled(); mAnimationScaleObserver = new AnimationScaleObserver(new Handler()); 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); mAccessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); mActivityLifecycleListeners = new ArrayList<>(); mFirstFrameListeners = new ArrayList<>(); // Create all platform channels accessibilityChannel = new AccessibilityChannel(dartExecutor); 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(platformPlugin); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mTextInputPlugin = new TextInputPlugin(this, dartExecutor); androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin); // Send initial platform information to Dart sendLocalesToDart(getResources().getConfiguration()); sendUserPlatformSettingsToDart(); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (!isAttached()) { return super.onKeyUp(keyCode, event); } androidKeyProcessor.onKeyUp(event); return super.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (!isAttached()) { return super.onKeyDown(keyCode, event); } androidKeyProcessor.onKeyDown(event); return 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() { updateAccessibilityFeatures(); for (ActivityLifecycleListener listener : mActivityLifecycleListeners) { listener.onPostResume(); } lifecycleChannel.appIsResumed(); } public void onStop() { lifecycleChannel.appIsPaused(); } public void onMemoryPressure() { systemChannel.sendMemoryPressureWarning(); } /** * 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. */ public void enableTransparentBackground() { 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(); } @SuppressWarnings("deprecation") private void sendLocalesToDart(Configuration config) { List locales = new ArrayList<>(); if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { LocaleList localeList = config.getLocales(); int localeCount = localeList.size(); for (int index = 0; index < localeCount; ++index) { Locale locale = localeList.get(index); locales.add(locale); } } else { locales.add(config.locale); } localizationChannel.sendLocales(locales); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); sendLocalesToDart(newConfig); sendUserPlatformSettingsToDart(); } float getDevicePixelRatio() { return mMetrics.devicePixelRatio; } public FlutterNativeView detach() { if (!isAttached()) return null; getHolder().removeCallback(mSurfaceCallback); mNativeView.detach(); FlutterNativeView view = mNativeView; mNativeView = null; return view; } public void destroy() { if (!isAttached()) return; getHolder().removeCallback(mSurfaceCallback); mNativeView.destroy(); mNativeView = null; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return mTextInputPlugin.createInputConnection(this, outAttrs); } // Must match the PointerChange enum in pointer.dart. private static final int kPointerChangeCancel = 0; private static final int kPointerChangeAdd = 1; private static final int kPointerChangeRemove = 2; private static final int kPointerChangeHover = 3; private static final int kPointerChangeDown = 4; private static final int kPointerChangeMove = 5; private static final int kPointerChangeUp = 6; // Must match the PointerDeviceKind enum in pointer.dart. private static final int kPointerDeviceKindTouch = 0; private static final int kPointerDeviceKindMouse = 1; private static final int kPointerDeviceKindStylus = 2; private static final int kPointerDeviceKindInvertedStylus = 3; private static final int kPointerDeviceKindSignal = 4; private static final int kPointerDeviceKindUnknown = 5; // Must match the PointerSignalKind enum in pointer.dart. private static final int kPointerSignalKindNone = 0; private static final int kPointerSignalKindScroll = 1; private static final int kPointerSignalKindUnknown = 2; // These values must match the unpacking code in hooks.dart. private static final int kPointerDataFieldCount = 24; private static final int kPointerBytesPerField = 8; private int getPointerChangeForAction(int maskedAction) { // Primary pointer: if (maskedAction == MotionEvent.ACTION_DOWN) { return kPointerChangeDown; } if (maskedAction == MotionEvent.ACTION_UP) { return kPointerChangeUp; } // Secondary pointer: if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) { return kPointerChangeDown; } if (maskedAction == MotionEvent.ACTION_POINTER_UP) { return kPointerChangeUp; } // All pointers: if (maskedAction == MotionEvent.ACTION_MOVE) { return kPointerChangeMove; } if (maskedAction == MotionEvent.ACTION_HOVER_MOVE) { return kPointerChangeHover; } if (maskedAction == MotionEvent.ACTION_CANCEL) { return kPointerChangeCancel; } return -1; } private int getPointerDeviceTypeForToolType(int toolType) { switch (toolType) { case MotionEvent.TOOL_TYPE_FINGER: return kPointerDeviceKindTouch; case MotionEvent.TOOL_TYPE_STYLUS: return kPointerDeviceKindStylus; case MotionEvent.TOOL_TYPE_MOUSE: return kPointerDeviceKindMouse; case MotionEvent.TOOL_TYPE_ERASER: return kPointerDeviceKindInvertedStylus; default: // MotionEvent.TOOL_TYPE_UNKNOWN will reach here. return kPointerDeviceKindUnknown; } } private void addPointerForIndex(MotionEvent event, int pointerIndex, int pointerChange, int pointerData, ByteBuffer packet) { if (pointerChange == -1) { return; } int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex)); int signalKind = kPointerSignalKindNone; long timeStamp = event.getEventTime() * 1000; // Convert from milliseconds to microseconds. packet.putLong(timeStamp); // time_stamp packet.putLong(pointerChange); // change packet.putLong(pointerKind); // kind packet.putLong(signalKind); // signal_kind packet.putLong(event.getPointerId(pointerIndex)); // device packet.putDouble(event.getX(pointerIndex)); // physical_x packet.putDouble(event.getY(pointerIndex)); // physical_y if (pointerKind == kPointerDeviceKindMouse) { packet.putLong(event.getButtonState() & 0x1F); // buttons } else if (pointerKind == kPointerDeviceKindStylus) { packet.putLong((event.getButtonState() >> 4) & 0xF); // buttons } else { packet.putLong(0); // buttons } packet.putLong(0); // obscured packet.putDouble(event.getPressure(pointerIndex)); // pressure double pressureMin = 0.0, pressureMax = 1.0; if (event.getDevice() != null) { InputDevice.MotionRange pressureRange = event.getDevice().getMotionRange(MotionEvent.AXIS_PRESSURE); if (pressureRange != null) { pressureMin = pressureRange.getMin(); pressureMax = pressureRange.getMax(); } } packet.putDouble(pressureMin); // pressure_min packet.putDouble(pressureMax); // pressure_max if (pointerKind == kPointerDeviceKindStylus) { packet.putDouble(event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerIndex)); // distance packet.putDouble(0.0); // distance_max } else { packet.putDouble(0.0); // distance packet.putDouble(0.0); // distance_max } packet.putDouble(event.getSize(pointerIndex)); // size packet.putDouble(event.getToolMajor(pointerIndex)); // radius_major packet.putDouble(event.getToolMinor(pointerIndex)); // radius_minor packet.putDouble(0.0); // radius_min packet.putDouble(0.0); // radius_max packet.putDouble(event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex)); // orientation if (pointerKind == kPointerDeviceKindStylus) { packet.putDouble(event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex)); // tilt } else { packet.putDouble(0.0); // tilt } packet.putLong(pointerData); // platformData packet.putDouble(0.0); // scroll_delta_x packet.putDouble(0.0); // scroll_delta_y } @Override public boolean onTouchEvent(MotionEvent event) { if (!isAttached()) { return false; } // 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); } // This value must match the value in framework's platform_view.dart. // This flag indicates whether the original Android pointer events were batched together. final int kPointerDataFlagBatched = 1; int pointerCount = event.getPointerCount(); ByteBuffer packet = ByteBuffer.allocateDirect(pointerCount * kPointerDataFieldCount * kPointerBytesPerField); packet.order(ByteOrder.LITTLE_ENDIAN); int maskedAction = event.getActionMasked(); int pointerChange = getPointerChangeForAction(event.getActionMasked()); if (maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN) { // ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only. addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); } else if (maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP) { // ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers. // We are converting these updates to move events here in order to preserve this data. // We also mark these events with a flag in order to help the framework reassemble // the original Android event later, should it need to forward it to a PlatformView. for (int p = 0; p < pointerCount; p++) { if (p != event.getActionIndex()) { if (event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) { addPointerForIndex(event, p, kPointerChangeMove, kPointerDataFlagBatched, packet); } } } // It's important that we're sending the UP event last. This allows PlatformView // to correctly batch everything back into the original Android event if needed. addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); } else { // ACTION_MOVE may not actually mean all pointers have moved // but it's the responsibility of a later part of the system to // ignore 0-deltas if desired. for (int p = 0; p < pointerCount; p++) { addPointerForIndex(event, p, pointerChange, 0, packet); } } if (packet.position() % (kPointerDataFieldCount * kPointerBytesPerField) != 0) { throw new AssertionError("Packet position is not on field boundary"); } mNativeView.getFlutterJNI().dispatchPointerDataPacket(packet, packet.position()); return true; } @Override public boolean onHoverEvent(MotionEvent event) { if (!isAttached()) { return false; } boolean handled = handleAccessibilityHoverEvent(event); if (!handled) { // TODO(ianh): Expose hover events to the platform, // implementing ADD, REMOVE, etc. } return handled; } @Override public boolean onGenericMotionEvent(MotionEvent event) { // Method isFromSource is only available in API 18+ (Jelly Bean MR2) // Mouse hover support is not implemented for API < 18. boolean isPointerEvent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && event.isFromSource(InputDevice.SOURCE_CLASS_POINTER); if (!isPointerEvent || event.getActionMasked() != MotionEvent.ACTION_HOVER_MOVE || !isAttached()) { return super.onGenericMotionEvent(event); } int pointerChange = getPointerChangeForAction(event.getActionMasked()); ByteBuffer packet = ByteBuffer.allocateDirect( event.getPointerCount() * kPointerDataFieldCount * kPointerBytesPerField); packet.order(ByteOrder.LITTLE_ENDIAN); // ACTION_HOVER_MOVE always applies to a single pointer only. addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); if (packet.position() % (kPointerDataFieldCount * kPointerBytesPerField) != 0) { throw new AssertionError("Packet position is not on field boundary"); } mNativeView.getFlutterJNI().dispatchPointerDataPacket(packet, packet.position()); return true; } @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. enum ZeroSides { NONE, LEFT, RIGHT, BOTH } 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. Activity activity = (Activity)getContext(); int orientation = activity.getResources().getConfiguration().orientation; int rotation = activity.getWindowManager().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 clean ways to detect keyboard instead of heuristics if possible // 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. int calculateBottomKeyboardInset(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. @Override public final WindowInsets onApplyWindowInsets(WindowInsets insets) { boolean statusBarHidden = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) != 0; boolean navigationBarHidden = (SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) != 0; // We zero the left and/or right sides to prevent the padding the // navigation bar would have caused. ZeroSides zeroSides = ZeroSides.NONE; if (navigationBarHidden) { zeroSides = calculateShouldZeroSides(); } // The padding on top should be removed when the statusbar is hidden. mMetrics.physicalPaddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop(); 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; // We perform hidden navbar and keyboard handling if the navbar is set to hidden. Otherwise, // the navbar padding should always be provided. mMetrics.physicalViewInsetBottom = navigationBarHidden ? calculateBottomKeyboardInset(insets) : insets.getSystemWindowInsetBottom(); 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(); } private void postRun() { } public void runFromBundle(FlutterRunArguments args) { assertAttached(); preRun(); mNativeView.runFromBundle(args); postRun(); } /** * @deprecated * Please use runFromBundle with `FlutterRunArguments`. */ @Deprecated public void runFromBundle(String bundlePath, String defaultPath) { runFromBundle(bundlePath, defaultPath, "main", false); } /** * @deprecated * Please use runFromBundle with `FlutterRunArguments`. */ @Deprecated public void runFromBundle(String bundlePath, String defaultPath, String entrypoint) { runFromBundle(bundlePath, defaultPath, entrypoint, false); } /** * @deprecated * Please use runFromBundle with `FlutterRunArguments`. * Parameter `reuseRuntimeController` has no effect. */ @Deprecated public void runFromBundle(String bundlePath, String defaultPath, String entrypoint, boolean reuseRuntimeController) { FlutterRunArguments args = new FlutterRunArguments(); args.bundlePath = bundlePath; args.entrypoint = entrypoint; args.defaultPath = defaultPath; runFromBundle(args); } /** * 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); WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); float fps = wm.getDefaultDisplay().getRefreshRate(); VsyncWaiter.refreshPeriodNanos = (long) (1000000000.0 / fps); VsyncWaiter.refreshRateFPS = fps; } // Called by native to update the semantics/accessibility tree. public void updateSemantics(ByteBuffer buffer, String[] strings) { try { if (mAccessibilityNodeProvider != null) { buffer.order(ByteOrder.LITTLE_ENDIAN); mAccessibilityNodeProvider.updateSemantics(buffer, strings); } } catch (Exception ex) { Log.e(TAG, "Uncaught exception while updating semantics", ex); } } public void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) { try { if (mAccessibilityNodeProvider != null) { buffer.order(ByteOrder.LITTLE_ENDIAN); mAccessibilityNodeProvider.updateCustomAccessibilityActions(buffer, strings); } } catch (Exception ex) { Log.e(TAG, "Uncaught exception while updating local context actions", ex); } } // Called by native to notify first Flutter frame rendered. public void onFirstFrame() { // Allow listeners to remove themselves when they are called. List listeners = new ArrayList<>(mFirstFrameListeners); for (FirstFrameListener listener : listeners) { listener.onFirstFrame(); } } // ACCESSIBILITY private boolean mAccessibilityEnabled = false; private boolean mTouchExplorationEnabled = false; private int mAccessibilityFeatureFlags = 0; private TouchExplorationListener mTouchExplorationListener; protected void dispatchSemanticsAction(int id, AccessibilityBridge.Action action) { dispatchSemanticsAction(id, action, null); } protected void dispatchSemanticsAction(int id, AccessibilityBridge.Action action, Object args) { if (!isAttached()) return; ByteBuffer encodedArgs = null; int position = 0; if (args != null) { encodedArgs = StandardMessageCodec.INSTANCE.encodeMessage(args); position = encodedArgs.position(); } mNativeView.getFlutterJNI().dispatchSemanticsAction(id, action.value, encodedArgs, position); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mAccessibilityEnabled = mAccessibilityManager.isEnabled(); mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE); getContext().getContentResolver().registerContentObserver(transitionUri, false, mAnimationScaleObserver); } if (mAccessibilityEnabled || mTouchExplorationEnabled) { ensureAccessibilityEnabled(); } if (mTouchExplorationEnabled) { mAccessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; } else { mAccessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; } // Apply additional accessibility settings updateAccessibilityFeatures(); resetWillNotDraw(); mAccessibilityManager.addAccessibilityStateChangeListener(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (mTouchExplorationListener == null) { mTouchExplorationListener = new TouchExplorationListener(); } mAccessibilityManager.addTouchExplorationStateChangeListener(mTouchExplorationListener); } } private void updateAccessibilityFeatures() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { String transitionAnimationScale = Settings.Global.getString(getContext().getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE); if (transitionAnimationScale != null && transitionAnimationScale.equals("0")) { mAccessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value; } else { mAccessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value; } } mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getContext().getContentResolver().unregisterContentObserver(mAnimationScaleObserver); mAccessibilityManager.removeAccessibilityStateChangeListener(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mAccessibilityManager.removeTouchExplorationStateChangeListener(mTouchExplorationListener); } } private void resetWillNotDraw() { if (!mIsSoftwareRenderingEnabled) { setWillNotDraw(!(mAccessibilityEnabled || mTouchExplorationEnabled)); } else { setWillNotDraw(false); } } @Override public void onAccessibilityStateChanged(boolean enabled) { if (enabled) { ensureAccessibilityEnabled(); } else { mAccessibilityEnabled = false; if (mAccessibilityNodeProvider != null) { mAccessibilityNodeProvider.setAccessibilityEnabled(false); } mNativeView.getFlutterJNI().setSemanticsEnabled(false); } resetWillNotDraw(); } /// Must match the enum defined in window.dart. private enum AccessibilityFeature { ACCESSIBLE_NAVIGATION(1 << 0), INVERT_COLORS(1 << 1), // NOT SUPPORTED DISABLE_ANIMATIONS(1 << 2); AccessibilityFeature(int value) { this.value = value; } final int value; } // Listens to the global TRANSITION_ANIMATION_SCALE property and notifies us so // that we can disable animations in Flutter. private class AnimationScaleObserver extends ContentObserver { public AnimationScaleObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange) { this.onChange(selfChange, null); } @Override public void onChange(boolean selfChange, Uri uri) { String value = Settings.Global.getString(getContext().getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE); if (value != null && value.equals("0")) { mAccessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value; } else { mAccessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value; } mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags); } } class TouchExplorationListener implements AccessibilityManager.TouchExplorationStateChangeListener { @Override public void onTouchExplorationStateChanged(boolean enabled) { if (enabled) { mTouchExplorationEnabled = true; ensureAccessibilityEnabled(); mAccessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags); } else { mTouchExplorationEnabled = false; if (mAccessibilityNodeProvider != null) { mAccessibilityNodeProvider.handleTouchExplorationExit(); } mAccessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; mNativeView.getFlutterJNI().setAccessibilityFeatures(mAccessibilityFeatureFlags); } resetWillNotDraw(); } } @Override public AccessibilityNodeProvider getAccessibilityNodeProvider() { if (mAccessibilityEnabled) return mAccessibilityNodeProvider; // TODO(goderbauer): when a11y is off this should return a one-off snapshot of // the a11y // tree. return null; } void ensureAccessibilityEnabled() { if (!isAttached()) return; mAccessibilityEnabled = true; if (mAccessibilityNodeProvider == null) { mAccessibilityNodeProvider = new AccessibilityBridge(this, accessibilityChannel); } mNativeView.getFlutterJNI().setSemanticsEnabled(true); mAccessibilityNodeProvider.setAccessibilityEnabled(true); } void resetAccessibilityTree() { if (mAccessibilityNodeProvider != null) { mAccessibilityNodeProvider.reset(); } } private boolean handleAccessibilityHoverEvent(MotionEvent event) { if (!mTouchExplorationEnabled) { return false; } if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { mAccessibilityNodeProvider.handleTouchExploration(event.getX(), event.getY()); } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) { mAccessibilityNodeProvider.handleTouchExplorationExit(); } else { Log.d("flutter", "unexpected accessibility hover event: " + event); return false; } return true; } @Override public void send(String channel, ByteBuffer message) { send(channel, message, null); } @Override 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 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); } } }