// Copyright 2013 The Chromium 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
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.plugin.common.*;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URI;
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";
private static final String ACTION_DISCOVER = "io.flutter.view.DISCOVER";
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 InputMethodManager mImm;
private final TextInputPlugin mTextInputPlugin;
private final SurfaceHolder.Callback mSurfaceCallback;
private final ViewportMetrics mMetrics;
private final AccessibilityManager mAccessibilityManager;
private final MethodChannel mFlutterLocalizationChannel;
private final MethodChannel mFlutterNavigationChannel;
private final BasicMessageChannel mFlutterKeyEventChannel;
private final BasicMessageChannel mFlutterLifecycleChannel;
private final BasicMessageChannel mFlutterSystemChannel;
private final BasicMessageChannel mFlutterSettingsChannel;
private final BroadcastReceiver mDiscoveryReceiver;
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 InputConnection mLastInputConnection;
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);
mIsSoftwareRenderingEnabled = nativeGetIsSoftwareRenderingEnabled();
mMetrics = new ViewportMetrics();
mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
setFocusable(true);
setFocusableInTouchMode(true);
Activity activity = (Activity) getContext();
if (nativeView == null) {
mNativeView = new FlutterNativeView(activity.getApplicationContext());
} else {
mNativeView = nativeView;
}
mNativeView.attachViewAndActivity(this, activity);
int color = 0xFF000000;
TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
color = typedValue.data;
}
// TODO(abarth): Consider letting the developer override this color.
final int backgroundColor = color;
mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
assertAttached();
nativeSurfaceCreated(mNativeView.get(), holder.getSurface(), backgroundColor);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
assertAttached();
nativeSurfaceChanged(mNativeView.get(), width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
assertAttached();
nativeSurfaceDestroyed(mNativeView.get());
}
};
getHolder().addCallback(mSurfaceCallback);
mAccessibilityManager =
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
mActivityLifecycleListeners = new ArrayList<>();
mFirstFrameListeners = new ArrayList<>();
// Configure the platform plugins and flutter channels.
mFlutterLocalizationChannel =
new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
mFlutterNavigationChannel =
new MethodChannel(this, "flutter/navigation", JSONMethodCodec.INSTANCE);
mFlutterKeyEventChannel =
new BasicMessageChannel<>(this, "flutter/keyevent", JSONMessageCodec.INSTANCE);
mFlutterLifecycleChannel =
new BasicMessageChannel<>(this, "flutter/lifecycle", StringCodec.INSTANCE);
mFlutterSystemChannel =
new BasicMessageChannel<>(this, "flutter/system", JSONMessageCodec.INSTANCE);
mFlutterSettingsChannel =
new BasicMessageChannel<>(this, "flutter/settings", JSONMessageCodec.INSTANCE);
PlatformPlugin platformPlugin = new PlatformPlugin(activity);
MethodChannel flutterPlatformChannel =
new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);
flutterPlatformChannel.setMethodCallHandler(platformPlugin);
addActivityLifecycleListener(platformPlugin);
mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
mTextInputPlugin = new TextInputPlugin(this);
setLocale(getResources().getConfiguration().locale);
setUserSettings();
if ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
mDiscoveryReceiver = new DiscoveryReceiver();
context.registerReceiver(mDiscoveryReceiver, new IntentFilter(ACTION_DISCOVER));
} else {
mDiscoveryReceiver = null;
}
}
private void encodeKeyEvent(KeyEvent event, Map message) {
message.put("flags", event.getFlags());
message.put("codePoint", event.getUnicodeChar());
message.put("keyCode", event.getKeyCode());
message.put("scanCode", event.getScanCode());
message.put("metaState", event.getMetaState());
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!isAttached()) {
return super.onKeyUp(keyCode, event);
}
Map message = new HashMap<>();
message.put("type", "keyup");
message.put("keymap", "android");
encodeKeyEvent(event, message);
mFlutterKeyEventChannel.send(message);
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (!isAttached()) {
return super.onKeyDown(keyCode, event);
}
if (event.getDeviceId() != KeyCharacterMap.VIRTUAL_KEYBOARD) {
if (mLastInputConnection != null && mImm.isAcceptingText()) {
mLastInputConnection.sendKeyEvent(event);
}
}
Map message = new HashMap<>();
message.put("type", "keydown");
message.put("keymap", "android");
encodeKeyEvent(event, message);
mFlutterKeyEventChannel.send(message);
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() {
mFlutterLifecycleChannel.send("AppLifecycleState.inactive");
}
public void onPause() {
mFlutterLifecycleChannel.send("AppLifecycleState.inactive");
}
public void onPostResume() {
for (ActivityLifecycleListener listener : mActivityLifecycleListeners) {
listener.onPostResume();
}
mFlutterLifecycleChannel.send("AppLifecycleState.resumed");
}
public void onStop() {
mFlutterLifecycleChannel.send("AppLifecycleState.paused");
}
public void onMemoryPressure() {
Map message = new HashMap<>(1);
message.put("type", "memoryPressure");
mFlutterSystemChannel.send(message);
}
/**
* 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);
}
public void setInitialRoute(String route) {
mFlutterNavigationChannel.invokeMethod("setInitialRoute", route);
}
public void pushRoute(String route) {
mFlutterNavigationChannel.invokeMethod("pushRoute", route);
}
public void popRoute() {
mFlutterNavigationChannel.invokeMethod("popRoute", null);
}
private void setUserSettings() {
Map message = new HashMap<>();
message.put("textScaleFactor", getResources().getConfiguration().fontScale);
message.put("alwaysUse24HourFormat", DateFormat.is24HourFormat(getContext()));
mFlutterSettingsChannel.send(message);
}
private void setLocale(Locale locale) {
mFlutterLocalizationChannel.invokeMethod(
"setLocale", Arrays.asList(locale.getLanguage(), locale.getCountry()));
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setLocale(newConfig.locale);
setUserSettings();
}
float getDevicePixelRatio() {
return mMetrics.devicePixelRatio;
}
public FlutterNativeView detach() {
if (!isAttached()) return null;
if (mDiscoveryReceiver != null) {
getContext().unregisterReceiver(mDiscoveryReceiver);
}
getHolder().removeCallback(mSurfaceCallback);
mNativeView.detach();
FlutterNativeView view = mNativeView;
mNativeView = null;
return view;
}
public void destroy() {
if (!isAttached()) return;
if (mDiscoveryReceiver != null) {
getContext().unregisterReceiver(mDiscoveryReceiver);
}
getHolder().removeCallback(mSurfaceCallback);
mNativeView.destroy();
mNativeView = null;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
try {
mLastInputConnection = mTextInputPlugin.createInputConnection(this, outAttrs);
return mLastInputConnection;
} catch (JSONException e) {
Log.e(TAG, "Failed to create input connection", e);
return null;
}
}
// 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 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_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;
default:
// MotionEvent.TOOL_TYPE_UNKNOWN will reach here.
return -1;
}
}
private void addPointerForIndex(MotionEvent event, int pointerIndex, ByteBuffer packet) {
int pointerChange = getPointerChangeForAction(event.getActionMasked());
if (pointerChange == -1) {
return;
}
int pointerKind = event.getToolType(pointerIndex);
if (pointerKind == -1) {
return;
}
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(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
// TODO(eseidel): Could get the calibrated range if necessary:
// event.getDevice().getMotionRange(MotionEvent.AXIS_PRESSURE)
packet.putDouble(event.getPressure(pointerIndex)); // pressure
packet.putDouble(0.0); // pressure_min
packet.putDouble(1.0); // 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.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
}
}
@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);
}
// These values must match the unpacking code in hooks.dart.
final int kPointerDataFieldCount = 19;
final int kBytePerField = 8;
int pointerCount = event.getPointerCount();
ByteBuffer packet =
ByteBuffer.allocateDirect(pointerCount * kPointerDataFieldCount * kBytePerField);
packet.order(ByteOrder.LITTLE_ENDIAN);
int maskedAction = event.getActionMasked();
// ACTION_UP, ACTION_POINTER_UP, ACTION_DOWN, and ACTION_POINTER_DOWN
// only apply to a single pointer, other events apply to all pointers.
if (maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP
|| maskedAction == MotionEvent.ACTION_DOWN
|| maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
addPointerForIndex(event, event.getActionIndex(), 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, packet);
}
}
assert packet.position() % (kPointerDataFieldCount * kBytePerField) == 0;
nativeDispatchPointerDataPacket(mNativeView.get(), 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
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
mMetrics.physicalWidth = width;
mMetrics.physicalHeight = height;
updateViewportMetrics();
super.onSizeChanged(width, height, oldWidth, oldHeight);
}
@Override
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
// Status bar, left/right system insets partially obscure content (padding).
mMetrics.physicalPaddingTop = insets.getSystemWindowInsetTop();
mMetrics.physicalPaddingRight = insets.getSystemWindowInsetRight();
mMetrics.physicalPaddingBottom = 0;
mMetrics.physicalPaddingLeft = insets.getSystemWindowInsetLeft();
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
mMetrics.physicalViewInsetTop = 0;
mMetrics.physicalViewInsetRight = 0;
mMetrics.physicalViewInsetBottom = 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(String bundlePath, String snapshotOverride) {
runFromBundle(bundlePath, snapshotOverride, "main", false);
}
public void runFromBundle(String bundlePath, String snapshotOverride, String entrypoint) {
runFromBundle(bundlePath, snapshotOverride, entrypoint, false);
}
public void runFromBundle(String bundlePath, String snapshotOverride, String entrypoint,
boolean reuseRuntimeController) {
assertAttached();
preRun();
mNativeView.runFromBundle(bundlePath, snapshotOverride, entrypoint, reuseRuntimeController);
postRun();
}
/**
* Return the most recent frame as a bitmap.
*
* @return A bitmap.
*/
public Bitmap getBitmap() {
assertAttached();
return nativeGetBitmap(mNativeView.get());
}
private static native void nativeSurfaceCreated(
long nativePlatformViewAndroid, Surface surface, int backgroundColor);
private static native void nativeSurfaceChanged(
long nativePlatformViewAndroid, int width, int height);
private static native void nativeSurfaceDestroyed(long nativePlatformViewAndroid);
private static native void nativeSetViewportMetrics(long nativePlatformViewAndroid,
float devicePixelRatio, int physicalWidth, int physicalHeight, int physicalPaddingTop,
int physicalPaddingRight, int physicalPaddingBottom, int physicalPaddingLeft,
int physicalViewInsetTop, int physicalViewInsetRight, int physicalViewInsetBottom,
int physicalViewInsetLeft);
private static native Bitmap nativeGetBitmap(long nativePlatformViewAndroid);
private static native void nativeDispatchPointerDataPacket(
long nativePlatformViewAndroid, ByteBuffer buffer, int position);
private static native void nativeDispatchSemanticsAction(
long nativePlatformViewAndroid, int id, int action, ByteBuffer args, int argsPosition);
private static native void nativeSetSemanticsEnabled(
long nativePlatformViewAndroid, boolean enabled);
private static native void nativeSetAssistiveTechnologyEnabled(
long nativePlatformViewAndroid, boolean enabled);
private static native boolean nativeGetIsSoftwareRenderingEnabled();
private static native void nativeRegisterTexture(
long nativePlatformViewAndroid, long textureId, SurfaceTexture surfaceTexture);
private static native void nativeMarkTextureFrameAvailable(
long nativePlatformViewAndroid, long textureId);
private static native void nativeUnregisterTexture(
long nativePlatformViewAndroid, long textureId);
private void updateViewportMetrics() {
if (!isAttached()) return;
nativeSetViewportMetrics(mNativeView.get(), 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);
}
// 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 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();
}
nativeDispatchSemanticsAction(mNativeView.get(), id, action.value, encodedArgs, position);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mAccessibilityEnabled = mAccessibilityManager.isEnabled();
mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
if (mAccessibilityEnabled || mTouchExplorationEnabled) {
ensureAccessibilityEnabled();
}
resetWillNotDraw();
mAccessibilityManager.addAccessibilityStateChangeListener(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (mTouchExplorationListener == null) {
mTouchExplorationListener = new TouchExplorationListener();
}
mAccessibilityManager.addTouchExplorationStateChangeListener(mTouchExplorationListener);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
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);
}
nativeSetSemanticsEnabled(mNativeView.get(), false);
}
resetWillNotDraw();
}
class TouchExplorationListener
implements AccessibilityManager.TouchExplorationStateChangeListener {
@Override
public void onTouchExplorationStateChanged(boolean enabled) {
if (enabled) {
mTouchExplorationEnabled = true;
ensureAccessibilityEnabled();
nativeSetAssistiveTechnologyEnabled(mNativeView.get(), true);
} else {
mTouchExplorationEnabled = false;
if (mAccessibilityNodeProvider != null) {
mAccessibilityNodeProvider.handleTouchExplorationExit();
}
nativeSetAssistiveTechnologyEnabled(mNativeView.get(), false);
}
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;
}
private AccessibilityBridge mAccessibilityNodeProvider;
void ensureAccessibilityEnabled() {
if (!isAttached()) return;
mAccessibilityEnabled = true;
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new AccessibilityBridge(this);
}
nativeSetSemanticsEnabled(mNativeView.get(), 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);
}
/**
* Broadcast receiver used to discover active Flutter instances.
*
* This is used by the `flutter` tool to find the observatory ports
* for all the active Flutter views. We dump the data to the logs
* and the tool scrapes the log lines for the data.
*/
private class DiscoveryReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
URI observatoryUri = URI.create(FlutterNativeView.getObservatoryUri());
JSONObject discover = new JSONObject();
try {
discover.put("id", getContext().getPackageName());
discover.put("observatoryPort", observatoryUri.getPort());
Log.i(TAG, "DISCOVER: " + discover); // The tool looks for this data. See
// android_device.dart.
} catch (JSONException e) {
}
}
}
/**
* 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);
nativeRegisterTexture(mNativeView.get(), 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;
this.surfaceTexture.setOnFrameAvailableListener(
new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture texture) {
nativeMarkTextureFrameAvailable(
mNativeView.get(), SurfaceTextureRegistryEntry.this.id);
}
});
}
@Override
public SurfaceTexture surfaceTexture() {
return surfaceTexture;
}
@Override
public long id() {
return id;
}
@Override
public void release() {
if (released) {
return;
}
released = true;
nativeUnregisterTexture(mNativeView.get(), id);
surfaceTexture.release();
}
}
}