diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 9eea6fcf21894..41f7d7481369c 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -603,7 +603,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugin FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java -FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index e63689a397503..ea1fd7b8f3afa 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -170,7 +170,8 @@ action("flutter_shell_java") { "io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java", "io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java", "io/flutter/embedding/engine/renderer/FlutterRenderer.java", - "io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java", + "io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java", + "io/flutter/embedding/engine/renderer/RenderSurface.java", "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java", "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", "io/flutter/embedding/engine/systemchannels/LifecycleChannel.java", @@ -412,6 +413,9 @@ action("robolectric_tests") { "test/io/flutter/embedding/android/FlutterActivityTest.java", "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", + "test/io/flutter/embedding/engine/FlutterJNITest.java", + "test/io/flutter/embedding/engine/RenderingComponentTest.java", + "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/util/PreconditionsTest.java", ] diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index aa7eb2dfe391e..396ad89a407ac 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -886,7 +886,18 @@ public boolean shouldAttachEngineToActivity() { } @Override - public void onFirstFrameRendered() {} + public void onFlutterUiDisplayed() { + // Notifies Android that we're fully drawn so that performance metrics can be collected by + // Flutter performance tests. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + reportFullyDrawn(); + } + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + // no-op + } /** * The mode of the background of a {@code FlutterActivity}, either opaque or transparent. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 863925e6ae9ca..506a2894780b2 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -25,7 +25,7 @@ import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; +import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.view.FlutterMain; @@ -83,10 +83,15 @@ private boolean isFlutterEngineFromHost; @NonNull - private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { + private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() { @Override - public void onFirstFrameRendered() { - host.onFirstFrameRendered(); + public void onFlutterUiDisplayed() { + host.onFlutterUiDisplayed(); + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + host.onFlutterUiNoLongerDisplayed(); } }; @@ -228,7 +233,7 @@ private void setupFlutterEngine() { *
* {@code inflater} and {@code container} may be null when invoked from an {@code Activity}. *
- * This method creates a new {@link FlutterView}, adds a {@link OnFirstFrameRenderedListener} to + * This method creates a new {@link FlutterView}, adds a {@link FlutterUiDisplayListener} to * it, and then returns it. */ @NonNull @@ -236,7 +241,7 @@ View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nulla Log.v(TAG, "Creating FlutterView."); ensureAlive(); flutterView = new FlutterView(host.getActivity(), host.getRenderMode(), host.getTransparencyMode()); - flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); + flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener); flutterSplashView = new FlutterSplashView(host.getContext()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { @@ -391,12 +396,12 @@ void onStop() { /** * Invoke this from {@code Activity#onDestroy()} or {@code Fragment#onDestroyView()}. *
- * This method removes this delegate's {@link FlutterView}'s {@link OnFirstFrameRenderedListener}.
+ * This method removes this delegate's {@link FlutterView}'s {@link FlutterUiDisplayListener}.
*/
void onDestroyView() {
Log.v(TAG, "onDestroyView()");
ensureAlive();
- flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
+ flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener);
}
/**
@@ -695,9 +700,13 @@ private void ensureAlive() {
boolean shouldAttachEngineToActivity();
/**
- * Invoked by this delegate when its {@link FlutterView} has rendered its first Flutter
- * frame.
+ * Invoked by this delegate when its {@link FlutterView} starts painting pixels.
+ */
+ void onFlutterUiDisplayed();
+
+ /**
+ * Invoked by this delegate when its {@link FlutterView} stops painting pixels.
*/
- void onFirstFrameRendered();
+ void onFlutterUiNoLongerDisplayed();
}
}
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
index 3ef682e53c4a6..3d274cbe2caf6 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
@@ -21,7 +21,7 @@
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
-import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.view.FlutterMain;
@@ -567,21 +567,6 @@ public
+ * This method forwards {@code onFlutterUiDisplayed()} to its attached {@code Activity}, if
+ * the attached {@code Activity} implements {@link FlutterUiDisplayListener}.
+ *
+ * Subclasses that override this method must call through to the {@code super} method.
+ *
+ * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
+ */
+ @Override
+ public void onFlutterUiDisplayed() {
+ FragmentActivity attachedActivity = getActivity();
+ if (attachedActivity instanceof FlutterUiDisplayListener) {
+ ((FlutterUiDisplayListener) attachedActivity).onFlutterUiDisplayed();
+ }
+ }
+
+ /**
+ * Invoked after the {@link FlutterView} within this {@code FlutterFragment} stops rendering
+ * pixels to the screen.
*
- * This method forwards {@code onFirstFrameRendered()} to its attached {@code Activity}, if
- * the attached {@code Activity} implements {@link OnFirstFrameRenderedListener}.
+ * This method forwards {@code onFlutterUiNoLongerDisplayed()} to its attached {@code Activity},
+ * if the attached {@code Activity} implements {@link FlutterUiDisplayListener}.
*
* Subclasses that override this method must call through to the {@code super} method.
*
* Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
- public void onFirstFrameRendered() {
+ public void onFlutterUiNoLongerDisplayed() {
FragmentActivity attachedActivity = getActivity();
- if (attachedActivity instanceof OnFirstFrameRenderedListener) {
- ((OnFirstFrameRenderedListener) attachedActivity).onFirstFrameRendered();
+ if (attachedActivity instanceof FlutterUiDisplayListener) {
+ ((FlutterUiDisplayListener) attachedActivity).onFlutterUiNoLongerDisplayed();
}
}
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java b/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java
index ec12e0e0e11b1..4820d829589a1 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java
@@ -16,7 +16,7 @@
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
-import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
/**
* {@code View} that displays a {@link SplashScreen} until a given {@link FlutterView}
@@ -51,13 +51,18 @@ public void onFlutterEngineDetachedFromFlutterView() {}
};
@NonNull
- private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
+ private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() {
@Override
- public void onFirstFrameRendered() {
+ public void onFlutterUiDisplayed() {
if (splashScreen != null) {
transitionToFlutter();
}
}
+
+ @Override
+ public void onFlutterUiNoLongerDisplayed() {
+ // no-op
+ }
};
@NonNull
@@ -114,7 +119,7 @@ public void displayFlutterViewWithSplash(
) {
// If we were displaying a previous FlutterView, remove it.
if (this.flutterView != null) {
- this.flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
+ this.flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener);
removeView(this.flutterView);
}
// If we were displaying a previous splash screen View, remove it.
@@ -136,7 +141,7 @@ public void displayFlutterViewWithSplash(
// waiting for the first frame to render. Show a splash UI until that happens.
splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
addView(this.splashScreenView);
- flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
+ flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
} else if (isSplashScreenTransitionNeededNow()) {
Log.v(TAG, "Showing an immediate splash transition to Flutter due to previously interrupted transition.");
splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
index dd9826e00216e..4d4e2df20300e 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
@@ -12,12 +12,10 @@
import android.view.SurfaceHolder;
import android.view.SurfaceView;
-import java.util.HashSet;
-import java.util.Set;
-
import io.flutter.Log;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
-import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
+import io.flutter.embedding.engine.renderer.RenderSurface;
/**
* Paints a Flutter UI on a {@link android.view.Surface}.
@@ -34,7 +32,7 @@
* desired, consider using a {@link FlutterView} which provides all of these behaviors and
* utilizes a {@code FlutterSurfaceView} internally.
*/
-public class FlutterSurfaceView extends SurfaceView implements FlutterRenderer.RenderSurface {
+public class FlutterSurfaceView extends SurfaceView implements RenderSurface {
private static final String TAG = "FlutterSurfaceView";
private final boolean renderTransparently;
@@ -42,8 +40,6 @@ public class FlutterSurfaceView extends SurfaceView implements FlutterRenderer.R
private boolean isAttachedToFlutterRenderer = false;
@Nullable
private FlutterRenderer flutterRenderer;
- @NonNull
- private Set
- * Flutter expects a user interface to exist on the platform side (Android), and that interface
- * is expected to offer some capabilities that Flutter depends upon. The {@link FlutterRenderer.RenderSurface}
- * interface represents those expectations.
- *
- * If an app includes a user interface that renders a Flutter UI then a {@link FlutterRenderer.RenderSurface}
- * should be set (this is the typical Flutter scenario). If no UI is being rendered, such as a
- * Flutter app that is running Dart code in the background, then no registration may be necessary.
- *
- * If no {@link FlutterRenderer.RenderSurface} is registered, then related messages coming from
- * Flutter will be dropped (ignored).
+ * Adds a {@link FlutterUiDisplayListener}, which receives a callback when Flutter's
+ * engine notifies {@code FlutterJNI} that Flutter is painting pixels to the {@link Surface} that
+ * was provided to Flutter.
*/
@UiThread
- public void setRenderSurface(@Nullable FlutterRenderer.RenderSurface renderSurface) {
+ public void addIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
ensureRunningOnMainThread();
- this.renderSurface = renderSurface;
+ flutterUiDisplayListeners.add(listener);
}
/**
- * Adds a {@link OnFirstFrameRenderedListener}, which receives a callback when Flutter's
- * engine notifies {@code FlutterJNI} that the first frame of a Flutter UI has been rendered
- * to the {@link Surface} that was provided to Flutter.
+ * Removes a {@link FlutterUiDisplayListener} that was added with
+ * {@link #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
*/
@UiThread
- public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
+ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
ensureRunningOnMainThread();
- firstFrameListeners.add(listener);
+ flutterUiDisplayListeners.remove(listener);
}
- /**
- * Removes a {@link OnFirstFrameRenderedListener} that was added with
- * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
- */
+ // Called by native to notify first Flutter frame rendered.
+ @SuppressWarnings("unused")
+ @VisibleForTesting
@UiThread
- public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
+ void onFirstFrame() {
ensureRunningOnMainThread();
- firstFrameListeners.remove(listener);
+
+ for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
+ listener.onFlutterUiDisplayed();
+ }
}
- // Called by native to notify first Flutter frame rendered.
- @SuppressWarnings("unused")
+ // TODO(mattcarroll): get native to call this when rendering stops.
+ @VisibleForTesting
@UiThread
- private void onFirstFrame() {
+ void onRenderingStopped() {
ensureRunningOnMainThread();
- if (renderSurface != null) {
- renderSurface.onFirstFrameRendered();
- }
- // TODO(mattcarroll): log dropped messages when in debug mode (https://github.com/flutter/flutter/issues/25391)
- for (OnFirstFrameRenderedListener listener : firstFrameListeners) {
- listener.onFirstFrameRendered();
+ for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) {
+ listener.onFlutterUiNoLongerDisplayed();
}
}
@@ -331,6 +318,7 @@ public void onSurfaceChanged(int width, int height) {
public void onSurfaceDestroyed() {
ensureRunningOnMainThread();
ensureAttachedToNative();
+ onRenderingStopped();
nativeSurfaceDestroyed(nativePlatformViewId);
}
@@ -431,7 +419,6 @@ private native void nativeDispatchPointerDataPacket(long nativePlatformViewId,
* See {@link AccessibilityBridge} for an example of an {@link AccessibilityDelegate} and the
* surrounding responsibilities.
*/
- // TODO(mattcarroll): move AccessibilityDelegate definition into FlutterJNI. FlutterJNI should be the basis of dependencies, not the other way round.
@UiThread
public void setAccessibilityDelegate(@Nullable AccessibilityDelegate accessibilityDelegate) {
ensureRunningOnMainThread();
@@ -772,7 +759,7 @@ public void addEngineLifecycleListener(@NonNull EngineLifecycleListener engineLi
/**
* Removes the given {@code engineLifecycleListener}, which was previously added using
- * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
+ * {@link #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
*/
@UiThread
public void removeEngineLifecycleListener(@NonNull EngineLifecycleListener engineLifecycleListener) {
diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
index bac9132df255f..306535a547a26 100644
--- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
+++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
@@ -17,95 +17,85 @@
import java.util.concurrent.atomic.AtomicLong;
import io.flutter.Log;
-import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.view.TextureRegistry;
/**
- * WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE.
- * IF YOU USE IT, WE WILL BREAK YOU.
- *
- * {@code FlutterRenderer} works in tandem with a provided {@link RenderSurface} to create an
- * interactive Flutter UI.
- *
- * {@code FlutterRenderer} manages textures for rendering, and forwards some Java calls to native Flutter
- * code via JNI. The corresponding {@link RenderSurface} is used as a delegate to carry out
- * certain actions on behalf of this {@code FlutterRenderer} within an Android view hierarchy.
- *
- * {@link FlutterView} is an implementation of a {@link RenderSurface}.
+ * Represents the rendering responsibilities of a {@code FlutterEngine}.
+ *
+ * {@code FlutterRenderer} works in tandem with a provided {@link RenderSurface} to paint Flutter
+ * pixels to an Android {@code View} hierarchy.
+ *
+ * {@code FlutterRenderer} manages textures for rendering, and forwards some Java calls to native
+ * Flutter code via JNI. The corresponding {@link RenderSurface} provides the Android
+ * {@link Surface} that this renderer paints.
+ *
+ * {@link io.flutter.embedding.android.FlutterSurfaceView} and
+ * {@link io.flutter.embedding.android.FlutterTextureView} are implementations of
+ * {@link RenderSurface}.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class FlutterRenderer implements TextureRegistry {
private static final String TAG = "FlutterRenderer";
+ @NonNull
private final FlutterJNI flutterJNI;
+ @NonNull
private final AtomicLong nextTextureId = new AtomicLong(0L);
- private RenderSurface renderSurface;
- private boolean hasRenderedFirstFrame = false;
+ @Nullable
+ private Surface surface;
+ private boolean isDisplayingFlutterUi = false;
- private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
+ @NonNull
+ private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() {
@Override
- public void onFirstFrameRendered() {
- hasRenderedFirstFrame = true;
+ public void onFlutterUiDisplayed() {
+ isDisplayingFlutterUi = true;
+ }
+
+ @Override
+ public void onFlutterUiNoLongerDisplayed() {
+ isDisplayingFlutterUi = false;
}
};
public FlutterRenderer(@NonNull FlutterJNI flutterJNI) {
this.flutterJNI = flutterJNI;
- this.flutterJNI.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
+ this.flutterJNI.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
}
/**
- * Returns true if this {@code FlutterRenderer} is attached to the given {@link RenderSurface},
- * false otherwise.
+ * Returns true if this {@code FlutterRenderer} is painting pixels to an Android {@code View}
+ * hierarchy, false otherwise.
*/
- public boolean isAttachedTo(@NonNull RenderSurface renderSurface) {
- return this.renderSurface == renderSurface;
- }
-
- public void attachToRenderSurface(@NonNull RenderSurface renderSurface) {
- Log.v(TAG, "Attaching to RenderSurface.");
- // TODO(mattcarroll): determine desired behavior when attaching to an already attached renderer
- if (this.renderSurface != null) {
- Log.v(TAG, "Already attached to a RenderSurface. Detaching from old one and attaching to new one.");
- detachFromRenderSurface();
- }
-
- this.renderSurface = renderSurface;
- this.renderSurface.attachToRenderer(this);
- this.flutterJNI.setRenderSurface(renderSurface);
- }
-
- public void detachFromRenderSurface() {
- Log.v(TAG, "Detaching from RenderSurface.");
- // TODO(mattcarroll): determine desired behavior if we're asked to detach without first being attached
- if (this.renderSurface != null) {
- this.renderSurface.detachFromRenderer();
- this.renderSurface = null;
- surfaceDestroyed();
- this.flutterJNI.setRenderSurface(null);
- }
+ public boolean isDisplayingFlutterUi() {
+ return isDisplayingFlutterUi;
}
- public boolean hasRenderedFirstFrame() {
- return hasRenderedFirstFrame;
- }
-
- public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
- flutterJNI.addOnFirstFrameRenderedListener(listener);
+ /**
+ * Adds a listener that is invoked whenever this {@code FlutterRenderer} starts and stops painting
+ * pixels to an Android {@code View} hierarchy.
+ */
+ public void addIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
+ flutterJNI.addIsDisplayingFlutterUiListener(listener);
- if (hasRenderedFirstFrame) {
- listener.onFirstFrameRendered();
+ if (isDisplayingFlutterUi) {
+ listener.onFlutterUiDisplayed();
}
}
- public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
- flutterJNI.removeOnFirstFrameRenderedListener(listener);
+ /**
+ * Removes a listener added via
+ * {@link #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}.
+ */
+ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListener listener) {
+ flutterJNI.removeIsDisplayingFlutterUiListener(listener);
}
//------ START TextureRegistry IMPLEMENTATION -----
- // TODO(mattcarroll): detachFromGLContext requires API 16. Create solution for earlier APIs.
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ /**
+ * Creates and returns a new {@link SurfaceTexture} that is also made available to Flutter code.
+ */
@Override
public SurfaceTextureEntry createSurfaceTexture() {
Log.v(TAG, "Creating a SurfaceTexture.");
@@ -180,19 +170,57 @@ public void release() {
}
//------ END TextureRegistry IMPLEMENTATION ----
- // TODO(mattcarroll): describe the native behavior that this invokes
- public void surfaceCreated(@NonNull Surface surface) {
+ /**
+ * Notifies Flutter that the given {@code surface} was created and is available for Flutter
+ * rendering.
+ *
+ * See {@link android.view.SurfaceHolder.Callback} and
+ * {@link android.view.TextureView.SurfaceTextureListener}
+ */
+ public void startRenderingToSurface(@NonNull Surface surface) {
+ if (this.surface != null) {
+ stopRenderingToSurface();
+ }
+
+ this.surface = surface;
+
flutterJNI.onSurfaceCreated(surface);
}
- // TODO(mattcarroll): describe the native behavior that this invokes
+ /**
+ * Notifies Flutter that a {@code surface} previously registered with
+ * {@link #startRenderingToSurface(Surface)} has changed size to the given {@code width} and
+ * {@code height}.
+ *
+ * See {@link android.view.SurfaceHolder.Callback} and
+ * {@link android.view.TextureView.SurfaceTextureListener}
+ */
public void surfaceChanged(int width, int height) {
flutterJNI.onSurfaceChanged(width, height);
}
- // TODO(mattcarroll): describe the native behavior that this invokes
- public void surfaceDestroyed() {
+ /**
+ * Notifies Flutter that a {@code surface} previously registered with
+ * {@link #startRenderingToSurface(Surface)} has been destroyed and needs to be released and
+ * cleaned up on the Flutter side.
+ *
+ * See {@link android.view.SurfaceHolder.Callback} and
+ * {@link android.view.TextureView.SurfaceTextureListener}
+ */
+ public void stopRenderingToSurface() {
flutterJNI.onSurfaceDestroyed();
+
+ surface = null;
+
+ // TODO(mattcarroll): the source of truth for this call should be FlutterJNI, which is where
+ // the call to onFlutterUiDisplayed() comes from. However, no such native callback exists yet,
+ // so until the engine and FlutterJNI are configured to call us back when rendering stops,
+ // we will manually monitor that change here.
+ if (isDisplayingFlutterUi) {
+ flutterUiDisplayListener.onFlutterUiNoLongerDisplayed();
+ }
+
+ isDisplayingFlutterUi = false;
}
// TODO(mattcarroll): describe the native behavior that this invokes
@@ -279,61 +307,6 @@ public void dispatchSemanticsAction(int id,
);
}
- /**
- * Delegate used in conjunction with a {@link FlutterRenderer} to create an interactive Flutter
- * UI.
- *
- * A {@code RenderSurface} is responsible for carrying out behaviors that are needed by a
- * corresponding {@link FlutterRenderer}.
- *
- * A {@code RenderSurface} also receives callbacks for important events, e.g.,
- * {@link #onFirstFrameRendered()}.
- */
- public interface RenderSurface {
- /**
- * Invoked by the owner of this {@code RenderSurface} when it wants to begin rendering
- * a Flutter UI to this {@code RenderSurface}.
- *
- * The details of how rendering is handled is an implementation detail.
- */
- void attachToRenderer(@NonNull FlutterRenderer renderer);
-
- /**
- * Invoked by the owner of this {@code RenderSurface} when it no longer wants to render
- * a Flutter UI to this {@code RenderSurface}.
- *
- * This method will cease any on-going rendering from Flutter to this {@code RenderSurface}.
- */
- void detachFromRenderer();
-
- // TODO(mattcarroll): convert old FlutterView to use FlutterEngine instead of individual
- // components, then use FlutterEngine's FlutterRenderer to watch for the first frame and
- // remove the following methods from this interface.
- /**
- * The {@link FlutterRenderer} corresponding to this {@code RenderSurface} has painted its
- * first frame since being initialized.
- *
- * "Initialized" refers to Flutter engine initialization, not the first frame after attaching
- * to the {@link FlutterRenderer}. Therefore, the first frame may have already rendered by
- * the time a {@code RenderSurface} has called {@link #attachToRenderSurface(RenderSurface)}
- * on a {@link FlutterRenderer}. In such a situation, {@code #onFirstFrameRendered()} will
- * never be called.
- */
- void onFirstFrameRendered();
-
- /**
- * Adds the given {@code listener} to this {@code FlutterRenderer}, to be notified upon Flutter's
- * first rendered frame.
- */
- void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener);
-
- /**
- * Removes the given {@code listener}, which was previously added with
- * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
- */
- void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener);
- }
-
/**
* Mutable data structure that holds all viewport metrics properties that Flutter cares about.
*
diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java
new file mode 100644
index 0000000000000..18fe8f8840778
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java
@@ -0,0 +1,24 @@
+// 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.embedding.engine.renderer;
+
+/**
+ * Listener invoked when Flutter starts and stops rendering pixels to an Android {@code View}
+ * hierarchy.
+ */
+public interface FlutterUiDisplayListener {
+ /**
+ * Flutter started painting pixels to an Android {@code View} hierarchy.
+ *
+ * This method will not be invoked if this listener is added after the {@link FlutterRenderer}
+ * has started painting pixels.
+ */
+ void onFlutterUiDisplayed();
+
+ /**
+ * Flutter stopped painting pixels to an Android {@code View} hierarchy.
+ */
+ void onFlutterUiNoLongerDisplayed();
+}
diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java b/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java
deleted file mode 100644
index 46d271b7f171b..0000000000000
--- a/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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.embedding.engine.renderer;
-
-/**
- * Listener invoked after Flutter paints its first frame since being initialized.
- *
- * WARNING: THIS CLASS IS EXPERIMENTAL. DO NOT SHIP A DEPENDENCY ON THIS CODE.
- * IF YOU USE IT, WE WILL BREAK YOU.
- */
-public interface OnFirstFrameRenderedListener {
- /**
- * A {@link FlutterRenderer} has painted its first frame since being initialized.
- *
- * This method will not be invoked if this listener is added after the first frame is rendered.
- */
- void onFirstFrameRendered();
-}
diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java b/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java
new file mode 100644
index 0000000000000..7e707b2f2188b
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java
@@ -0,0 +1,55 @@
+// 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.embedding.engine.renderer;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+
+/**
+ * Owns a {@code Surface} that {@code FlutterRenderer} would like to paint.
+ *
+ * {@code RenderSurface} is responsible for providing a {@code Surface} to a given
+ * {@code FlutterRenderer} when requested, and then notify that {@code FlutterRenderer} when
+ * the {@code Surface} changes, or is destroyed.
+ *
+ * The behavior of providing a {@code Surface} is delegated to this interface because the timing
+ * of a {@code Surface}'s availability is determined by Android. Therefore, an accessor method
+ * would not fulfill the requirements. Therefore, a {@code RenderSurface} is given a
+ * {@code FlutterRenderer}, which the {@code RenderSurface} is expected to notify as a
+ * {@code Surface} becomes available, changes, or is destroyed.
+ */
+public interface RenderSurface {
+ /**
+ * Returns the {@code FlutterRenderer} that is attached to this {@code RenderSurface}, or
+ * null if no {@code FlutterRenderer} is currently attached.
+ */
+ @Nullable
+ FlutterRenderer getAttachedRenderer();
+
+ /**
+ * Instructs this {@code RenderSurface} to give its {@code Surface} to the given
+ * {@code FlutterRenderer} so that Flutter can paint pixels on it.
+ *
+ * After this call, {@code RenderSurface} is expected to invoke the following methods on
+ * {@link FlutterRenderer} at the appropriate times:
+ *
+ * This {@code RenderSurface} should also clean up any references related to the previously
+ * connected {@code FlutterRenderer}.
+ */
+ void detachFromRenderer();
+}
diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java
index 639c61a83ede9..49adf56116e8e 100644
--- a/shell/platform/android/io/flutter/view/FlutterNativeView.java
+++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java
@@ -13,16 +13,9 @@
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener;
import io.flutter.embedding.engine.dart.DartExecutor;
-import io.flutter.embedding.engine.renderer.FlutterRenderer;
-import io.flutter.embedding.engine.renderer.FlutterRenderer.RenderSurface;
-import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
import io.flutter.plugin.common.*;
import java.nio.ByteBuffer;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.HashMap;
-import java.util.Map;
-
-import io.flutter.embedding.engine.dart.PlatformMessageHandler;
public class FlutterNativeView implements BinaryMessenger {
private static final String TAG = "FlutterNativeView";
@@ -34,6 +27,21 @@ public class FlutterNativeView implements BinaryMessenger {
private final Context mContext;
private boolean applicationIsRunning;
+ private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() {
+ @Override
+ public void onFlutterUiDisplayed() {
+ if (mFlutterView == null) {
+ return;
+ }
+ mFlutterView.onFirstFrame();
+ }
+
+ @Override
+ public void onFlutterUiNoLongerDisplayed() {
+ // no-op
+ }
+ };
+
public FlutterNativeView(@NonNull Context context) {
this(context, false);
}
@@ -42,7 +50,7 @@ public FlutterNativeView(@NonNull Context context, boolean isBackgroundView) {
mContext = context;
mPluginRegistry = new FlutterPluginRegistry(this, context);
mFlutterJNI = new FlutterJNI();
- mFlutterJNI.setRenderSurface(new RenderSurfaceImpl());
+ mFlutterJNI.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
this.dartExecutor = new DartExecutor(mFlutterJNI, context.getAssets());
mFlutterJNI.addEngineLifecycleListener(new EngineLifecycleListenerImpl());
attach(this, isBackgroundView);
@@ -58,6 +66,7 @@ public void destroy() {
mPluginRegistry.destroy();
dartExecutor.onDetachedFromJNI();
mFlutterView = null;
+ mFlutterJNI.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
mFlutterJNI.detachFromNativeAndReleaseResources();
applicationIsRunning = false;
}
@@ -143,48 +152,6 @@ private void attach(FlutterNativeView view, boolean isBackgroundView) {
dartExecutor.onAttachedToJNI();
}
- private final class RenderSurfaceImpl implements RenderSurface {
- @Override
- public void attachToRenderer(@NonNull FlutterRenderer renderer) {
- // Not relevant for v1 embedding.
- }
-
- @Override
- public void detachFromRenderer() {
- // Not relevant for v1 embedding.
- }
-
- // Called by native to update the semantics/accessibility tree.
- public void updateSemantics(ByteBuffer buffer, String[] strings) {
- if (mFlutterView == null) {
- return;
- }
- mFlutterView.updateSemantics(buffer, strings);
- }
-
- // Called by native to update the custom accessibility actions.
- public void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) {
- if (mFlutterView == null) {
- return;
- }
- mFlutterView.updateCustomAccessibilityActions(buffer, strings);
- }
-
- // Called by native to notify first Flutter frame rendered.
- public void onFirstFrameRendered() {
- if (mFlutterView == null) {
- return;
- }
- mFlutterView.onFirstFrame();
- }
-
- @Override
- public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {}
-
- @Override
- public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {}
- }
-
private final class EngineLifecycleListenerImpl implements EngineLifecycleListener {
// Called by native to notify when the engine is restarted (cold reload).
@SuppressWarnings("unused")
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index f10a72414fafa..d1037170e1911 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -679,29 +679,6 @@ private void updateViewportMetrics() {
);
}
- // 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 FlutterNativeView to notify first Flutter frame rendered.
public void onFirstFrame() {
didRenderFirstFrame = true;
diff --git a/shell/platform/android/io/flutter/view/TextureRegistry.java b/shell/platform/android/io/flutter/view/TextureRegistry.java
index ed3134bb035dd..01c76e0335a97 100644
--- a/shell/platform/android/io/flutter/view/TextureRegistry.java
+++ b/shell/platform/android/io/flutter/view/TextureRegistry.java
@@ -6,6 +6,7 @@
import android.graphics.SurfaceTexture;
+// TODO(mattcarroll): re-evalute docs in this class and add nullability annotations.
/**
* Registry of backend textures used with a single {@link FlutterView} instance.
* Entries may be embedded into the Flutter view using the
diff --git a/shell/platform/android/platform_view_android_jni.cc b/shell/platform/android/platform_view_android_jni.cc
index f2d17dd13fbe1..8b70d145dc439 100644
--- a/shell/platform/android/platform_view_android_jni.cc
+++ b/shell/platform/android/platform_view_android_jni.cc
@@ -486,7 +486,7 @@ static void InvokePlatformMessageEmptyResponseCallback(JNIEnv* env,
bool RegisterApi(JNIEnv* env) {
static const JNINativeMethod flutter_jni_methods[] = {
- // Start of methods from FlutterNativeView
+ // Start of methods from FlutterJNI
{
.name = "nativeAttach",
.signature = "(Lio/flutter/embedding/engine/FlutterJNI;Z)J",
diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
index 0f1bc37871aaf..036cbd56173b9 100644
--- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java
+++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java
@@ -13,7 +13,10 @@
import io.flutter.embedding.android.FlutterFragmentTest;
import io.flutter.embedding.engine.FlutterEngineCacheTest;
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
+import io.flutter.embedding.engine.RenderingComponentTest;
+import io.flutter.embedding.engine.renderer.FlutterRendererTest;
import io.flutter.util.PreconditionsTest;
+import io.flutter.embedding.engine.FlutterJNITest;
@RunWith(Suite.class)
@SuiteClasses({
@@ -23,6 +26,9 @@
FlutterFragmentTest.class,
// FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this
FlutterEngineCacheTest.class,
+ FlutterJNITest.class,
+ RenderingComponentTest.class,
+ FlutterRendererTest.class,
PlatformChannelTest.class
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java
new file mode 100644
index 0000000000000..962c732815f9b
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java
@@ -0,0 +1,48 @@
+package io.flutter.embedding.engine;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
+
+import static org.junit.Assert.assertEquals;
+
+@Config(manifest=Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class FlutterJNITest {
+ @Test
+ public void itAllowsFirstFrameListenersToRemoveThemselvesInline() {
+ // --- Test Setup ---
+ FlutterJNI flutterJNI = new FlutterJNI();
+
+ AtomicInteger callbackInvocationCount = new AtomicInteger(0);
+ FlutterUiDisplayListener callback = new FlutterUiDisplayListener() {
+ @Override
+ public void onFlutterUiDisplayed() {
+ callbackInvocationCount.incrementAndGet();
+ flutterJNI.removeIsDisplayingFlutterUiListener(this);
+ }
+
+ @Override
+ public void onFlutterUiNoLongerDisplayed() {}
+ };
+ flutterJNI.addIsDisplayingFlutterUiListener(callback);
+
+ // --- Execute Test ---
+ flutterJNI.onFirstFrame();
+
+ // --- Verify Results ---
+ assertEquals(1, callbackInvocationCount.get());
+
+ // --- Execute Test ---
+ // The callback removed itself from the listener list. A second call doesn't call the callback.
+ flutterJNI.onFirstFrame();
+
+ // --- Verify Results ---
+ assertEquals(1, callbackInvocationCount.get());
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/RenderingComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/RenderingComponentTest.java
new file mode 100644
index 0000000000000..bd7cdbfec98ed
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/RenderingComponentTest.java
@@ -0,0 +1,93 @@
+package io.flutter.embedding.engine;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.flutter.embedding.engine.renderer.FlutterRenderer;
+import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@Config(manifest=Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class RenderingComponentTest {
+ @Test
+ public void flutterUiDisplayListenersCanRemoveThemselvesWhenInvoked() {
+ // Setup test.
+ FlutterJNI flutterJNI = new FlutterJNI();
+ FlutterRenderer flutterRenderer = new FlutterRenderer(flutterJNI);
+
+ AtomicInteger listenerInvocationCount = new AtomicInteger(0);
+ FlutterUiDisplayListener listener = new FlutterUiDisplayListener() {
+ @Override
+ public void onFlutterUiDisplayed() {
+ // This is the behavior we're testing, but we also verify that this method
+ // was invoked to ensure that this test behavior executed.
+ flutterRenderer.removeIsDisplayingFlutterUiListener(this);
+
+ // Track the invocation to ensure this method is called once, and only once.
+ listenerInvocationCount.incrementAndGet();
+ }
+
+ @Override
+ public void onFlutterUiNoLongerDisplayed() {}
+ };
+ flutterRenderer.addIsDisplayingFlutterUiListener(listener);
+
+ // Execute behavior under test.
+ // Pretend we are the native side and tell FlutterJNI that Flutter has rendered a frame.
+ flutterJNI.onFirstFrame();
+
+ // Verify results.
+ // If we got to this point without an exception, and if our listener was called one time,
+ // then the behavior under test is correct.
+ assertEquals(1, listenerInvocationCount.get());
+ }
+
+ @Test
+ public void flutterUiDisplayListenersAddedAfterFirstFrameAreAutomaticallyInvoked() {
+ // Setup test.
+ FlutterJNI flutterJNI = new FlutterJNI();
+ FlutterRenderer flutterRenderer = new FlutterRenderer(flutterJNI);
+
+ FlutterUiDisplayListener listener = mock(FlutterUiDisplayListener.class);
+
+ // Pretend we are the native side and tell FlutterJNI that Flutter has rendered a frame.
+ flutterJNI.onFirstFrame();
+
+ // Execute behavior under test.
+ flutterRenderer.addIsDisplayingFlutterUiListener(listener);
+
+ // Verify results.
+ verify(listener, times(1)).onFlutterUiDisplayed();
+ }
+
+ @Test
+ public void flutterUiDisplayListenersAddedAfterFlutterUiDisappearsAreNotInvoked() {
+ // Setup test.
+ FlutterJNI flutterJNI = new FlutterJNI();
+ FlutterRenderer flutterRenderer = new FlutterRenderer(flutterJNI);
+
+ FlutterUiDisplayListener listener = mock(FlutterUiDisplayListener.class);
+
+ // Pretend we are the native side and tell FlutterJNI that Flutter has rendered a frame.
+ flutterJNI.onFirstFrame();
+
+ // Pretend that rendering has stopped.
+ flutterJNI.onRenderingStopped();
+
+ // Execute behavior under test.
+ flutterRenderer.addIsDisplayingFlutterUiListener(listener);
+
+ // Verify results.
+ verify(listener, never()).onFlutterUiDisplayed();
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java
new file mode 100644
index 0000000000000..5c2d7a31551bb
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java
@@ -0,0 +1,104 @@
+package io.flutter.embedding.engine.renderer;
+
+import android.view.Surface;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import io.flutter.embedding.engine.FlutterJNI;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@Config(manifest=Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class FlutterRendererTest {
+
+ private FlutterJNI fakeFlutterJNI;
+ private Surface fakeSurface;
+
+ @Before
+ public void setup() {
+ fakeFlutterJNI = mock(FlutterJNI.class);
+ fakeSurface = mock(Surface.class);
+ }
+
+ @Test
+ public void itForwardsSurfaceCreationNotificationToFlutterJNI() {
+ // Setup the test.
+ Surface fakeSurface = mock(Surface.class);
+ FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
+
+ // Execute the behavior under test.
+ flutterRenderer.startRenderingToSurface(fakeSurface);
+
+ // Verify the behavior under test.
+ verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface));
+ }
+
+ @Test
+ public void itForwardsSurfaceChangeNotificationToFlutterJNI() {
+ // Setup the test.
+ Surface fakeSurface = mock(Surface.class);
+ FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
+
+ flutterRenderer.startRenderingToSurface(fakeSurface);
+
+ // Execute the behavior under test.
+ flutterRenderer.surfaceChanged(100, 50);
+
+ // Verify the behavior under test.
+ verify(fakeFlutterJNI, times(1)).onSurfaceChanged(eq(100), eq(50));
+ }
+
+ @Test
+ public void itForwardsSurfaceDestructionNotificationToFlutterJNI() {
+ // Setup the test.
+ Surface fakeSurface = mock(Surface.class);
+ FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
+
+ flutterRenderer.startRenderingToSurface(fakeSurface);
+
+ // Execute the behavior under test.
+ flutterRenderer.stopRenderingToSurface();
+
+ // Verify the behavior under test.
+ verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
+ }
+
+ @Test
+ public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() {
+ // Setup the test.
+ Surface fakeSurface2 = mock(Surface.class);
+ FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
+
+ flutterRenderer.startRenderingToSurface(fakeSurface);
+
+ // Execute behavior under test.
+ flutterRenderer.startRenderingToSurface(fakeSurface2);
+
+ // Verify behavior under test.
+ verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); // notification of 1st surface's removal.
+ }
+
+ @Test
+ public void itStopsRenderingToSurfaceWhenRequested() {
+ // Setup the test.
+ FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
+
+ flutterRenderer.startRenderingToSurface(fakeSurface);
+
+ // Execute the behavior under test.
+ flutterRenderer.stopRenderingToSurface();
+
+ // Verify behavior under test.
+ verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
+ }
+}
*
*
* To facilitate platform messages between Java and Dart running in Flutter, register a handler:
@@ -155,15 +156,13 @@ private static void asyncWaitForVsync(final long cookie) {
@Nullable
private Long nativePlatformViewId;
@Nullable
- private FlutterRenderer.RenderSurface renderSurface;
- @Nullable
private AccessibilityDelegate accessibilityDelegate;
@Nullable
private PlatformMessageHandler platformMessageHandler;
@NonNull
- private final Set
+ *
+ */
+ void attachToRenderer(@NonNull FlutterRenderer renderer);
+
+ /**
+ * Instructs this {@code RenderSurface} to stop forwarding {@code Surface} notifications to the
+ * {@code FlutterRenderer} that was previously connected with
+ * {@link #attachToRenderer(FlutterRenderer)}.
+ *