diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java new file mode 100644 index 0000000000000..56a89554457bd --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterEngineRule.java @@ -0,0 +1,84 @@ +package io.flutter.embedding.engine.renderer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +/** + * Prepares and returns a {@link FlutterEngine} and {@link Intent} primed with an engine for tests. + */ +public final class FlutterEngineRule extends TestWatcher { + private static final String cachedEngineId = "flutter_engine_rule_cached_engine"; + private final Context ctx = ApplicationProvider.getApplicationContext(); + private FlutterJNI flutterJNI; + private FlutterEngine flutterEngine; + private boolean jniIsAttached = true; + + @Override + protected void starting(Description description) { + // Setup mock JNI. + flutterJNI = mock(FlutterJNI.class); + when(flutterJNI.isAttached()).thenAnswer(i -> jniIsAttached); + + // We will not try to load plugins in these tests. + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(false); + + // Create an engine. + flutterEngine = new FlutterEngine(ctx, mockFlutterLoader, flutterJNI); + + // Place it in the engine cache. + FlutterEngineCache.getInstance().put(cachedEngineId, flutterEngine); + } + + @Override + protected void finished(Description description) { + FlutterEngineCache.getInstance().clear(); + } + + /** + * Returns a Mockito-mocked version of {@link FlutterJNI}. + * + * @return an instance that is already considered attached. + */ + FlutterJNI getFlutterJNI() { + return this.flutterJNI; + } + + /** + * Returns a pre-configured engine. + * + * @return flutter engine using the mock provided by {{@link #getFlutterJNI()}}. + */ + FlutterEngine getFlutterEngine() { + return this.flutterEngine; + } + + /** + * Sets what {@link FlutterJNI#isAttached()} returns. If not invoked, defaults to true. + * + * @param isAttached whether to consider JNI attached. + */ + void setJniIsAttached(boolean isAttached) { + this.jniIsAttached = isAttached; + } + + /** + * Creates an intent with {@link FlutterEngine} instance already provided. + * + * @return intent, i.e. to use with {@link androidx.test.ext.junit.rules.ActivityScenarioRule}. + */ + Intent makeIntent() { + return FlutterActivity.withCachedEngine(cachedEngineId).build(ctx); + } +} 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 index f19da788d3759..ed79d7b6a08f0 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -14,7 +14,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.graphics.Canvas; @@ -23,12 +22,16 @@ import android.media.Image; import android.os.Looper; import android.view.Surface; +import androidx.lifecycle.Lifecycle; +import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.view.TextureRegistry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -37,10 +40,14 @@ @Config(manifest = Config.NONE) @RunWith(AndroidJUnit4.class) public class FlutterRendererTest { + @Rule(order = 1) + public final FlutterEngineRule engineRule = new FlutterEngineRule(); + + @Rule(order = 2) + public final ActivityScenarioRule scenarioRule = + new ActivityScenarioRule<>(engineRule.makeIntent()); private FlutterJNI fakeFlutterJNI; - private Surface fakeSurface; - private Surface fakeSurface2; @Before public void init() { @@ -50,16 +57,14 @@ public void init() { @Before public void setup() { - fakeFlutterJNI = mock(FlutterJNI.class); - fakeSurface = mock(Surface.class); - fakeSurface2 = mock(Surface.class); + fakeFlutterJNI = engineRule.getFlutterJNI(); } @Test public void itForwardsSurfaceCreationNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); // Execute the behavior under test. flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -72,7 +77,7 @@ public void itForwardsSurfaceCreationNotificationToFlutterJNI() { public void itForwardsSurfaceChangeNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -87,7 +92,7 @@ public void itForwardsSurfaceChangeNotificationToFlutterJNI() { public void itForwardsSurfaceDestructionNotificationToFlutterJNI() { // Setup the test. Surface fakeSurface = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -101,8 +106,9 @@ public void itForwardsSurfaceDestructionNotificationToFlutterJNI() { @Test public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() { // Setup the test. + Surface fakeSurface = mock(Surface.class); Surface fakeSurface2 = mock(Surface.class); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -116,7 +122,8 @@ public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() { @Test public void itStopsRenderingToSurfaceWhenRequested() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -130,10 +137,10 @@ public void itStopsRenderingToSurfaceWhenRequested() { @Test public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, false); // Verify behavior under test. @@ -143,10 +150,10 @@ public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() { @Test public void itNeverStopsRenderingToSurfaceWhenRequested() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, true); // Verify behavior under test. @@ -156,13 +163,11 @@ public void itNeverStopsRenderingToSurfaceWhenRequested() { @Test public void itStopsSurfaceTextureCallbackWhenDetached() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); - flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. @@ -175,9 +180,8 @@ public void itStopsSurfaceTextureCallbackWhenDetached() { @Test public void itRegistersExistingSurfaceTexture() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); SurfaceTexture surfaceTexture = new SurfaceTexture(0); @@ -197,11 +201,8 @@ public void itRegistersExistingSurfaceTexture() { @Test public void itUnregistersTextureWhenSurfaceTextureFinalized() { // Setup the test. - FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); - when(fakeFlutterJNI.isAttached()).thenReturn(true); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -223,18 +224,15 @@ public void itUnregistersTextureWhenSurfaceTextureFinalized() { @Test public void itStopsUnregisteringTextureWhenDetached() { // Setup the test. - FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); - when(fakeFlutterJNI.isAttached()).thenReturn(false); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - - fakeFlutterJNI.detachFromNativeAndReleaseResources(); + Surface fakeSurface = mock(Surface.class); + engineRule.setJniIsAttached(false); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); long id = entry.id(); flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.stopRenderingToSurface(); // Execute the behavior under test. @@ -246,18 +244,17 @@ public void itStopsUnregisteringTextureWhenDetached() { verify(fakeFlutterJNI, times(0)).unregisterTexture(eq(id)); } + /** @noinspection FinalizeCalledExplicitly */ void runFinalization(FlutterRenderer.SurfaceTextureRegistryEntry entry) { CountDownLatch latch = new CountDownLatch(1); Thread fakeFinalizer = new Thread( - new Runnable() { - public void run() { - try { - entry.finalize(); - latch.countDown(); - } catch (Throwable e) { - // do nothing - } + () -> { + try { + entry.finalize(); + latch.countDown(); + } catch (Throwable e) { + // do nothing } }); fakeFinalizer.start(); @@ -270,8 +267,14 @@ public void run() { @Test public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { - // Setup the test. + // Intentionally do not use 'engineRule' in this test, because we are testing a very narrow + // API (the side-effects of 'setViewportMetrics'). Under normal construction, the engine will + // invoke 'setViewportMetrics' a number of times automatically, making testing the side-effects + // of the method call more difficult than needed. + FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + + // Setup the test. FlutterRenderer.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics(); metrics.width = 1000; metrics.height = 1000; @@ -332,16 +335,10 @@ public void itConvertsDisplayFeatureArrayToPrimitiveArrays() { @Test public void itNotifyImageFrameListener() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); AtomicInteger invocationCount = new AtomicInteger(0); - final TextureRegistry.OnFrameConsumedListener listener = - new TextureRegistry.OnFrameConsumedListener() { - @Override - public void onFrameConsumed() { - invocationCount.incrementAndGet(); - } - }; + final TextureRegistry.OnFrameConsumedListener listener = invocationCount::incrementAndGet; FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -357,7 +354,7 @@ public void onFrameConsumed() { @Test public void itAddsListenerWhenSurfaceTextureEntryCreated() { // Setup the test. - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(fakeFlutterJNI)); + FlutterRenderer flutterRenderer = spy(engineRule.getFlutterEngine().getRenderer()); // Execute the behavior under test. FlutterRenderer.SurfaceTextureRegistryEntry entry = @@ -370,7 +367,7 @@ public void itAddsListenerWhenSurfaceTextureEntryCreated() { @Test public void itRemovesListenerWhenSurfaceTextureEntryReleased() { // Setup the test. - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(fakeFlutterJNI)); + FlutterRenderer flutterRenderer = spy(engineRule.getFlutterEngine().getRenderer()); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -384,16 +381,11 @@ public void itRemovesListenerWhenSurfaceTextureEntryReleased() { @Test public void itNotifySurfaceTextureEntryWhenMemoryPressureWarning() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); AtomicInteger invocationCount = new AtomicInteger(0); final TextureRegistry.OnTrimMemoryListener listener = - new TextureRegistry.OnTrimMemoryListener() { - @Override - public void onTrimMemory(int level) { - invocationCount.incrementAndGet(); - } - }; + level -> invocationCount.incrementAndGet(); FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); @@ -409,7 +401,8 @@ public void onTrimMemory(int level) { @Test public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { // Setup the test. - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); @@ -424,7 +417,8 @@ public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { @Test public void itInvokesCreatesSurfaceWhenStartingRendering() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); flutterRenderer.startRenderingToSurface(fakeSurface, false); verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); @@ -432,7 +426,9 @@ public void itInvokesCreatesSurfaceWhenStartingRendering() { @Test public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + Surface fakeSurface = mock(Surface.class); + Surface fakeSurface2 = mock(Surface.class); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); // The following call sequence mimics the behaviour of FlutterView when it exits from hybrid // composition mode. @@ -458,9 +454,10 @@ public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { @Test public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -481,6 +478,7 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { // Extract the image and check its size. Image image = texture.acquireLatestImage(); + assert image != null; assertEquals(1, image.getWidth()); assertEquals(1, image.getHeight()); image.close(); @@ -500,6 +498,7 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { // Extract the image and check its size. image = texture.acquireLatestImage(); + assert image != null; assertEquals(5, image.getWidth()); assertEquals(5, image.getHeight()); image.close(); @@ -510,10 +509,11 @@ public void ImageReaderSurfaceProducerProducesImageOfCorrectSize() { } @Test - public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInflight() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInFlight() { + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -541,9 +541,10 @@ public void ImageReaderSurfaceProducerDoesNotDropFramesWhenResizeInflight() { @Test public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + (FlutterRenderer.ImageReaderSurfaceProducer) producer; texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -623,9 +624,11 @@ public void ImageReaderSurfaceProducerImageReadersAndImagesCount() { @Test public void ImageReaderSurfaceProducerTrimMemoryCallback() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + (FlutterRenderer.ImageReaderSurfaceProducer) producer; + texture.disableFenceForTest(); // Returns a null image when one hasn't been produced. @@ -687,37 +690,46 @@ public void ImageReaderSurfaceProducerTrimMemoryCallback() { // A 0x0 ImageReader is a runtime error. @Test public void ImageReaderSurfaceProducerClampsWidthAndHeightTo1() { - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - FlutterRenderer.ImageReaderSurfaceProducer texture = - flutterRenderer.new ImageReaderSurfaceProducer(0); + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); // Default values. - assertEquals(texture.getWidth(), 1); - assertEquals(texture.getHeight(), 1); + assertEquals(producer.getWidth(), 1); + assertEquals(producer.getHeight(), 1); // Try setting width and height to 0. - texture.setSize(0, 0); + producer.setSize(0, 0); // Ensure we can still create/get a surface without an exception being raised. - assertNotNull(texture.getSurface()); + assertNotNull(producer.getSurface()); // Expect clamp to 1. - assertEquals(texture.getWidth(), 1); - assertEquals(texture.getHeight(), 1); + assertEquals(producer.getWidth(), 1); + assertEquals(producer.getHeight(), 1); } @Test public void SurfaceTextureSurfaceProducerCreatesAConnectedTexture() { // Force creating a SurfaceTextureSurfaceProducer regardless of Android API version. - FlutterRenderer.debugForceSurfaceProducerGlTextures = true; + Surface fakeSurface = mock(Surface.class); + try { + FlutterRenderer.debugForceSurfaceProducerGlTextures = true; + FlutterRenderer flutterRenderer = engineRule.getFlutterEngine().getRenderer(); + TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); - FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - TextureRegistry.SurfaceProducer producer = flutterRenderer.createSurfaceProducer(); + flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, false); + // Verify behavior under test. + assertEquals(producer.id(), 0); + verify(fakeFlutterJNI, times(1)).registerTexture(eq(producer.id()), any()); + } finally { + FlutterRenderer.debugForceSurfaceProducerGlTextures = false; + } + } - // Verify behavior under test. - assertEquals(producer.id(), 0); - verify(fakeFlutterJNI, times(1)).registerTexture(eq(producer.id()), any()); + @Test + public void CanLaunchActivityUsingFlutterEngine() { + // This is a placeholder test that will be used to test lifecycle events w/ SurfaceProducer. + scenarioRule.getScenario().moveToState(Lifecycle.State.RESUMED); } } diff --git a/shell/platform/android/test_runner/src/main/AndroidManifest.xml b/shell/platform/android/test_runner/src/main/AndroidManifest.xml index 57bdeee858695..65ca9afc77113 100644 --- a/shell/platform/android/test_runner/src/main/AndroidManifest.xml +++ b/shell/platform/android/test_runner/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + +