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 T build() { // implementation for details about why it exists. private FlutterActivityAndFragmentDelegate delegate; - private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { - @Override - public void onFirstFrameRendered() { - // Notify our subclasses that the first frame has been rendered. - FlutterFragment.this.onFirstFrameRendered(); - - // Notify our owning Activity that the first frame has been rendered. - FragmentActivity fragmentActivity = getActivity(); - if (fragmentActivity instanceof OnFirstFrameRenderedListener) { - OnFirstFrameRenderedListener activityAsListener = (OnFirstFrameRenderedListener) fragmentActivity; - activityAsListener.onFirstFrameRendered(); - } - } - }; - public FlutterFragment() { // Ensure that we at least have an empty Bundle of arguments so that we don't // need to continually check for null arguments before grabbing one. @@ -951,21 +936,40 @@ public boolean shouldAttachEngineToActivity() { } /** - * Invoked after the {@link FlutterView} within this {@code FlutterFragment} renders its first - * frame. + * Invoked after the {@link FlutterView} within this {@code FlutterFragment} starts rendering + * pixels to the screen. + *

+ * 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 onFirstFrameRenderedListeners = new HashSet<>(); // Connects the {@code Surface} beneath this {@code SurfaceView} with Flutter's native code. // Callbacks are received by this Object and then those messages are forwarded to our @@ -51,7 +47,7 @@ public class FlutterSurfaceView extends SurfaceView implements FlutterRenderer.R private final SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { - Log.v(TAG, "SurfaceHolder.Callback.surfaceCreated()"); + Log.v(TAG, "SurfaceHolder.Callback.startRenderingToSurface()"); isSurfaceAvailableForRendering = true; if (isAttachedToFlutterRenderer) { @@ -69,7 +65,7 @@ public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) { - Log.v(TAG, "SurfaceHolder.Callback.surfaceDestroyed()"); + Log.v(TAG, "SurfaceHolder.Callback.stopRenderingToSurface()"); isSurfaceAvailableForRendering = false; if (isAttachedToFlutterRenderer) { @@ -78,6 +74,24 @@ public void surfaceDestroyed(@NonNull SurfaceHolder holder) { } }; + private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() { + @Override + public void onFlutterUiDisplayed() { + Log.v(TAG, "onFlutterUiDisplayed()"); + // Now that a frame is ready to display, take this SurfaceView from transparent to opaque. + setAlpha(1.0f); + + if (flutterRenderer != null) { + flutterRenderer.removeIsDisplayingFlutterUiListener(this); + } + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + // no-op + } + }; + /** * Constructs a {@code FlutterSurfaceView} programmatically, without any XML attributes. */ @@ -123,6 +137,12 @@ private void init() { setAlpha(0.0f); } + @Nullable + @Override + public FlutterRenderer getAttachedRenderer() { + return flutterRenderer; + } + /** * Invoked by the owner of this {@code FlutterSurfaceView} when it wants to begin rendering * a Flutter UI to this {@code FlutterSurfaceView}. @@ -140,12 +160,15 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { Log.v(TAG, "Attaching to FlutterRenderer."); if (this.flutterRenderer != null) { Log.v(TAG, "Already connected to a FlutterRenderer. Detaching from old one and attaching to new one."); - this.flutterRenderer.detachFromRenderSurface(); + this.flutterRenderer.stopRenderingToSurface(); + this.flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); } this.flutterRenderer = flutterRenderer; isAttachedToFlutterRenderer = true; + this.flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener); + // If we're already attached to an Android window then we're now attached to both a renderer // and the Android window. We can begin rendering now. if (isSurfaceAvailableForRendering) { @@ -173,6 +196,8 @@ public void detachFromRenderer() { // Make the SurfaceView invisible to avoid showing a black rectangle. setAlpha(0.0f); + this.flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); + flutterRenderer = null; isAttachedToFlutterRenderer = false; } else { @@ -186,7 +211,7 @@ private void connectSurfaceToRenderer() { throw new IllegalStateException("connectSurfaceToRenderer() should only be called when flutterRenderer and getHolder() are non-null."); } - flutterRenderer.surfaceCreated(getHolder().getSurface()); + flutterRenderer.startRenderingToSurface(getHolder().getSurface()); } // FlutterRenderer must be non-null. @@ -205,36 +230,6 @@ private void disconnectSurfaceFromRenderer() { throw new IllegalStateException("disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null."); } - flutterRenderer.surfaceDestroyed(); - } - - /** - * Adds the given {@code listener} to this {@code FlutterSurfaceView}, to be notified upon Flutter's - * first rendered frame. - */ - @Override - public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.add(listener); - } - - /** - * Removes the given {@code listener}, which was previously added with - * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}. - */ - @Override - public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.remove(listener); - } - - @Override - public void onFirstFrameRendered() { - // TODO(mattcarroll): decide where this method should live and what it needs to do. - Log.v(TAG, "onFirstFrameRendered()"); - // Now that a frame is ready to display, take this SurfaceView from transparent to opaque. - setAlpha(1.0f); - - for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) { - listener.onFirstFrameRendered(); - } + flutterRenderer.stopRenderingToSurface(); } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java index 14758eb8154d9..7205cc5a6f970 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java @@ -12,12 +12,9 @@ import android.view.Surface; import android.view.TextureView; -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.RenderSurface; /** * Paints a Flutter UI on a {@link SurfaceTexture}. @@ -34,15 +31,13 @@ * desired, consider using a {@link FlutterView} which provides all of these behaviors and * utilizes a {@code FlutterTextureView} internally. */ -public class FlutterTextureView extends TextureView implements FlutterRenderer.RenderSurface { +public class FlutterTextureView extends TextureView implements RenderSurface { private static final String TAG = "FlutterTextureView"; private boolean isSurfaceAvailableForRendering = false; private boolean isAttachedToFlutterRenderer = false; @Nullable private FlutterRenderer flutterRenderer; - @NonNull - private Set onFirstFrameRenderedListeners = new HashSet<>(); // Connects the {@code SurfaceTexture} beneath this {@code TextureView} with Flutter's native code. // Callbacks are received by this Object and then those messages are forwarded to our @@ -111,6 +106,12 @@ private void init() { setSurfaceTextureListener(surfaceTextureListener); } + @Nullable + @Override + public FlutterRenderer getAttachedRenderer() { + return flutterRenderer; + } + /** * Invoked by the owner of this {@code FlutterTextureView} when it wants to begin rendering * a Flutter UI to this {@code FlutterTextureView}. @@ -128,7 +129,7 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { Log.v(TAG, "Attaching to FlutterRenderer."); if (this.flutterRenderer != null) { Log.v(TAG, "Already connected to a FlutterRenderer. Detaching from old one and attaching to new one."); - this.flutterRenderer.detachFromRenderSurface(); + this.flutterRenderer.stopRenderingToSurface(); } this.flutterRenderer = flutterRenderer; @@ -171,7 +172,7 @@ private void connectSurfaceToRenderer() { throw new IllegalStateException("connectSurfaceToRenderer() should only be called when flutterRenderer and getSurfaceTexture() are non-null."); } - flutterRenderer.surfaceCreated(new Surface(getSurfaceTexture())); + flutterRenderer.startRenderingToSurface(new Surface(getSurfaceTexture())); } // FlutterRenderer must be non-null. @@ -190,34 +191,6 @@ private void disconnectSurfaceFromRenderer() { throw new IllegalStateException("disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null."); } - flutterRenderer.surfaceDestroyed(); - } - - /** - * Adds the given {@code listener} to this {@code FlutterTextureView}, to be notified upon Flutter's - * first rendered frame. - */ - @Override - public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.add(listener); - } - - /** - * Removes the given {@code listener}, which was previously added with - * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}. - */ - @Override - public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.remove(listener); - } - - @Override - public void onFirstFrameRendered() { - // TODO(mattcarroll): decide where this method should live and what it needs to do. - Log.v(TAG, "onFirstFrameRendered()"); - - for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) { - listener.onFirstFrameRendered(); - } + flutterRenderer.stopRenderingToSurface(); } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 1d007a35aee18..08ba9ddce6b5a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -37,7 +37,8 @@ import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; 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; import io.flutter.plugin.editing.TextInputPlugin; import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.view.AccessibilityBridge; @@ -74,9 +75,9 @@ public class FlutterView extends FrameLayout { // Internal view hierarchy references. @Nullable - private FlutterRenderer.RenderSurface renderSurface; - private final Set onFirstFrameRenderedListeners = new HashSet<>(); - private boolean didRenderFirstFrame; + private RenderSurface renderSurface; + private final Set flutterUiDisplayListeners = new HashSet<>(); + private boolean isFlutterUiDisplayed; // Connections to a Flutter execution context. @Nullable @@ -108,13 +109,22 @@ public void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTou } }; - private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { + private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() { @Override - public void onFirstFrameRendered() { - didRenderFirstFrame = true; + public void onFlutterUiDisplayed() { + isFlutterUiDisplayed = true; - for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) { - listener.onFirstFrameRendered(); + for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) { + listener.onFlutterUiDisplayed(); + } + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + isFlutterUiDisplayed = false; + + for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) { + listener.onFlutterUiNoLongerDisplayed(); } } }; @@ -228,23 +238,23 @@ private void init() { * */ public boolean hasRenderedFirstFrame() { - return didRenderFirstFrame; + return isFlutterUiDisplayed; } /** * Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's * first rendered frame. */ - public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.add(listener); + public void addOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) { + flutterUiDisplayListeners.add(listener); } /** * Removes the given {@code listener}, which was previously added with - * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}. + * {@link #addOnFirstFrameRenderedListener(FlutterUiDisplayListener)}. */ - public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { - onFirstFrameRenderedListeners.remove(listener); + public void removeOnFirstFrameRenderedListener(@NonNull FlutterUiDisplayListener listener) { + flutterUiDisplayListeners.remove(listener); } //------- Start: Process View configuration that Flutter cares about. ------ @@ -580,9 +590,9 @@ public void attachToFlutterEngine( // Instruct our FlutterRenderer that we are now its designated RenderSurface. FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer(); - didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame(); - flutterRenderer.attachToRenderSurface(renderSurface); - flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); + isFlutterUiDisplayed = flutterRenderer.isDisplayingFlutterUi(); + renderSurface.attachToRenderer(flutterRenderer); + flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener); // Initialize various components that know how to process Android View I/O // in a way that Flutter understands. @@ -631,8 +641,8 @@ public void attachToFlutterEngine( // If the first frame has already been rendered, notify all first frame listeners. // Do this after all other initialization so that listeners don't inadvertently interact // with a FlutterView that is only partially attached to a FlutterEngine. - if (didRenderFirstFrame) { - onFirstFrameRenderedListener.onFirstFrameRendered(); + if (isFlutterUiDisplayed) { + flutterUiDisplayListener.onFlutterUiDisplayed(); } } @@ -674,9 +684,9 @@ public void detachFromFlutterEngine() { // Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface. FlutterRenderer flutterRenderer = flutterEngine.getRenderer(); - didRenderFirstFrame = false; - flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener); - flutterRenderer.detachFromRenderSurface(); + isFlutterUiDisplayed = false; + flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); + flutterRenderer.stopRenderingToSurface(); flutterEngine = null; } @@ -685,7 +695,8 @@ public void detachFromFlutterEngine() { */ @VisibleForTesting public boolean isAttachedToFlutterEngine() { - return flutterEngine != null && flutterEngine.getRenderer().isAttachedTo(renderSurface); + return flutterEngine != null + && flutterEngine.getRenderer() == renderSurface.getAttachedRenderer(); } /** diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 3c91fdd3a1050..618dec5a1f99e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -20,6 +20,7 @@ import io.flutter.embedding.engine.plugins.contentprovider.ContentProviderControlSurface; import io.flutter.embedding.engine.plugins.service.ServiceControlSurface; import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; @@ -51,8 +52,8 @@ * invoked twice on the same {@code FlutterEngine}. * * To start rendering Flutter content to the screen, use {@link #getRenderer()} to obtain a - * {@link FlutterRenderer} and then attach a {@link FlutterRenderer.RenderSurface}. Consider using - * a {@link io.flutter.embedding.android.FlutterView} as a {@link FlutterRenderer.RenderSurface}. + * {@link FlutterRenderer} and then attach a {@link RenderSurface}. Consider using + * a {@link io.flutter.embedding.android.FlutterView} as a {@link RenderSurface}. */ // TODO(mattcarroll): re-evaluate system channel APIs - some are not well named or differentiated public class FlutterEngine implements LifecycleOwner { @@ -118,8 +119,8 @@ public void onPreEngineRestart() { * to begin executing Dart code within this {@code FlutterEngine}. * * A new {@code FlutterEngine} will not display any UI until a - * {@link io.flutter.embedding.engine.renderer.FlutterRenderer.RenderSurface} is registered. See - * {@link #getRenderer()} and {@link FlutterRenderer#attachToRenderSurface(FlutterRenderer.RenderSurface)}. + * {@link RenderSurface} is registered. See + * {@link #getRenderer()} and {@link FlutterRenderer#startRenderingToSurface(RenderSurface)}. * * A new {@code FlutterEngine} does not come with any Flutter plugins attached. To attach plugins, * see {@link #getPlugins()}. @@ -221,7 +222,7 @@ public DartExecutor getDartExecutor() { * The rendering system associated with this {@code FlutterEngine}. * * To render a Flutter UI that is produced by this {@code FlutterEngine}'s Dart code, attach - * a {@link io.flutter.embedding.engine.renderer.FlutterRenderer.RenderSurface} to this + * a {@link RenderSurface} to this * {@link FlutterRenderer}. */ @NonNull diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index fbd63888c19d1..a7006912d0205 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -12,18 +12,19 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.support.annotation.VisibleForTesting; import android.view.Surface; import android.view.SurfaceHolder; import java.nio.ByteBuffer; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; import io.flutter.embedding.engine.dart.PlatformMessageHandler; -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; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.view.AccessibilityBridge; import io.flutter.view.FlutterCallbackInformation; @@ -72,13 +73,13 @@ * } * * To provide a visual, interactive surface for Flutter rendering and touch events, register a - * {@link FlutterRenderer.RenderSurface} with {@link #setRenderSurface(FlutterRenderer.RenderSurface)} + * {@link RenderSurface} with {@link #setRenderSurface(RenderSurface)} * * To receive callbacks for certain events that occur on the native side, register listeners: * *

    *
  1. {@link #addEngineLifecycleListener(EngineLifecycleListener)}
  2. - *
  3. {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}
  4. + *
  5. {@link #addIsDisplayingFlutterUiListener(FlutterUiDisplayListener)}
  6. *
* * 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 engineLifecycleListeners = new HashSet<>(); + private final Set engineLifecycleListeners = new CopyOnWriteArraySet<>(); @NonNull - private final Set firstFrameListeners = new HashSet<>(); + private final Set flutterUiDisplayListeners = new CopyOnWriteArraySet<>(); @NonNull private final Looper mainLooper; // cached to avoid synchronization on repeat access. @@ -233,58 +232,46 @@ private void ensureAttachedToNative() { //----- Start Render Surface Support ----- /** - * Sets the {@link FlutterRenderer.RenderSurface} delegate for the attached Flutter context. - *

- * 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: + *

    + *
  1. {@link FlutterRenderer#startRenderingToSurface(Surface)}
  2. + *
  3. {@link FlutterRenderer#surfaceChanged(int, int)}}
  4. + *
  5. {@link FlutterRenderer#stopRenderingToSurface()}
  6. + *
+ */ + 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)}. + *

+ * 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(); + } +}