diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index a5ad8f4650839..80d4046d3e4ec 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -407,6 +407,8 @@ 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/renderer/FlutterRendererTest.java", "test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java", "test/io/flutter/util/PreconditionsTest.java", ] diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index fbd63888c19d1..01c068fe97261 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -12,12 +12,14 @@ 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.CopyOnWriteArrayList; import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; @@ -275,15 +277,17 @@ public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedList // Called by native to notify first Flutter frame rendered. @SuppressWarnings("unused") + @VisibleForTesting @UiThread - private void onFirstFrame() { + protected void onFirstFrame() { 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) { + CopyOnWriteArrayList mutableListeners = + new CopyOnWriteArrayList<>(firstFrameListeners); + for (OnFirstFrameRenderedListener listener : mutableListeners) { listener.onFirstFrameRendered(); } } diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 6c95754ccb402..747af35ec5620 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -12,6 +12,8 @@ import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; +import io.flutter.embedding.engine.FlutterJNITest; +import io.flutter.embedding.engine.renderer.FlutterRendererTest; import io.flutter.embedding.engine.systemchannels.TextInputChannelTest; import io.flutter.util.PreconditionsTest; @@ -23,6 +25,8 @@ FlutterFragmentTest.class, // FlutterActivityAndFragmentDelegateTest.class, TODO(mklim): Fix and re-enable this FlutterEngineCacheTest.class, + FlutterJNITest.class, + FlutterRendererTest.class, TextInputChannelTest.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..c35846ea530d9 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java @@ -0,0 +1,57 @@ +package io.flutter.embedding.engine; + +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 io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.atomic.AtomicInteger; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterJNITest { + FlutterJNI jniUnderTest; + + @Before + public void setUp() { + jniUnderTest = new FlutterJNI(); + } + + @Test + public void itAllowsFirstFrameListenersToRemoveThemselvesInline() { + // --- Test Setup --- + AtomicInteger callbackCalled = new AtomicInteger(0); + OnFirstFrameRenderedListener callback = new OnFirstFrameRenderedListener() { + @Override + public void onFirstFrameRendered() { + callbackCalled.incrementAndGet(); + jniUnderTest.removeOnFirstFrameRenderedListener(this); + }; + }; + jniUnderTest.addOnFirstFrameRenderedListener(callback); + + // --- Execute Test --- + jniUnderTest.onFirstFrame(); + + // --- Verify Results --- + assertEquals(1, callbackCalled.get()); + + // --- Execute Test --- + // The callback removed itself from the listener list. A second call doesn't call the callback. + jniUnderTest.onFirstFrame(); + + // --- Verify Results --- + assertEquals(1, callbackCalled.get()); + } +} 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..dd9c31097b3f3 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -0,0 +1,66 @@ +package io.flutter.embedding.engine.renderer; + +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 io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.atomic.AtomicInteger; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterRendererTest { + FlutterJNITest jniUnderTest; + FlutterRenderer rendererUnderTest; + + @Before + public void setUp() { + jniUnderTest = new FlutterJNITest(); + rendererUnderTest = new FlutterRenderer(jniUnderTest); + } + + @Test + public void itAllowsFirstFrameListenersToRemoveThemselvesInline() { + // --- Test Setup --- + AtomicInteger callbackCalled = new AtomicInteger(0); + OnFirstFrameRenderedListener callback = new OnFirstFrameRenderedListener() { + @Override + public void onFirstFrameRendered() { + callbackCalled.incrementAndGet(); + rendererUnderTest.removeOnFirstFrameRenderedListener(this); + }; + }; + rendererUnderTest.addOnFirstFrameRenderedListener(callback); + + // --- Execute Test --- + jniUnderTest.onFirstFrame(); + + // --- Verify Results --- + assertEquals(1, callbackCalled.get()); + + // --- Execute Test --- + // The callback removed itself from the listener list. A second call doesn't call the callback. + jniUnderTest.onFirstFrame(); + + // --- Verify Results --- + assertEquals(1, callbackCalled.get()); + } + + private static class FlutterJNITest extends FlutterJNI { + protected void onFirstFrame() { + // Exposed here for tests. + super.onFirstFrame(); + } + } +} diff --git a/testing/run_tests.py b/testing/run_tests.py index 37951ed70beb3..dabeeb9a7ce21 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -205,7 +205,7 @@ def EnsureDebugUnoptSkyPackagesAreBuilt(): def EnsureJavaTestsAreBuilt(android_out_dir): ninja_command = [ - 'ninja', + 'autoninja', '-C', android_out_dir, 'flutter/shell/platform/android:robolectric_tests'