diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 4529db72d86..2d56b234806 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -28,12 +28,7 @@ import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import io.flutter.plugin.common.EventChannel; import io.flutter.view.TextureRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; final class VideoPlayer { @@ -48,9 +43,7 @@ final class VideoPlayer { private final TextureRegistry.SurfaceTextureEntry textureEntry; - private QueuingEventSink eventSink; - - private final EventChannel eventChannel; + private final VideoPlayerCallbacks videoPlayerEvents; private static final String USER_AGENT = "User-Agent"; @@ -62,13 +55,13 @@ final class VideoPlayer { VideoPlayer( Context context, - EventChannel eventChannel, + VideoPlayerCallbacks events, TextureRegistry.SurfaceTextureEntry textureEntry, String dataSource, String formatHint, @NonNull Map httpHeaders, VideoPlayerOptions options) { - this.eventChannel = eventChannel; + this.videoPlayerEvents = events; this.textureEntry = textureEntry; this.options = options; @@ -86,24 +79,23 @@ final class VideoPlayer { exoPlayer.setMediaItem(mediaItem); exoPlayer.prepare(); - setUpVideoPlayer(exoPlayer, new QueuingEventSink()); + setUpVideoPlayer(exoPlayer); } // Constructor used to directly test members of this class. @VisibleForTesting VideoPlayer( ExoPlayer exoPlayer, - EventChannel eventChannel, + VideoPlayerCallbacks events, TextureRegistry.SurfaceTextureEntry textureEntry, VideoPlayerOptions options, - QueuingEventSink eventSink, DefaultHttpDataSource.Factory httpDataSourceFactory) { - this.eventChannel = eventChannel; + this.videoPlayerEvents = events; this.textureEntry = textureEntry; this.options = options; this.httpDataSourceFactory = httpDataSourceFactory; - setUpVideoPlayer(exoPlayer, eventSink); + setUpVideoPlayer(exoPlayer); } @VisibleForTesting @@ -118,37 +110,29 @@ public void configureHttpDataSourceFactory(@NonNull Map httpHead httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty); } - private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) { + private void setUpVideoPlayer(ExoPlayer exoPlayer) { this.exoPlayer = exoPlayer; - this.eventSink = eventSink; - - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink.setDelegate(sink); - } - - @Override - public void onCancel(Object o) { - eventSink.setDelegate(null); - } - }); surface = new Surface(textureEntry.surfaceTexture()); exoPlayer.setVideoSurface(surface); setAudioAttributes(exoPlayer, options.mixWithOthers); + // Avoids synthetic accessor. + VideoPlayerCallbacks events = this.videoPlayerEvents; + exoPlayer.addListener( new Listener() { private boolean isBuffering = false; public void setBuffering(boolean buffering) { - if (isBuffering != buffering) { - isBuffering = buffering; - Map event = new HashMap<>(); - event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd"); - eventSink.success(event); + if (isBuffering == buffering) { + return; + } + isBuffering = buffering; + if (buffering) { + events.onBufferingStart(); + } else { + events.onBufferingEnd(); } } @@ -163,11 +147,8 @@ public void onPlaybackStateChanged(final int playbackState) { sendInitialized(); } } else if (playbackState == Player.STATE_ENDED) { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); + events.onCompleted(); } - if (playbackState != Player.STATE_BUFFERING) { setBuffering(false); } @@ -180,30 +161,20 @@ public void onPlayerError(@NonNull final PlaybackException error) { // See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window exoPlayer.seekToDefaultPosition(); exoPlayer.prepare(); - } else if (eventSink != null) { - eventSink.error("VideoError", "Video player had error " + error, null); + } else { + events.onError("VideoError", "Video player had error " + error, null); } } @Override public void onIsPlayingChanged(boolean isPlaying) { - if (eventSink != null) { - Map event = new HashMap<>(); - event.put("event", "isPlayingStateUpdate"); - event.put("isPlaying", isPlaying); - eventSink.success(event); - } + events.onIsPlayingStateUpdate(isPlaying); } }); } void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); + videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition()); } private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { @@ -248,35 +219,29 @@ long getPosition() { @SuppressWarnings("SuspiciousNameCombination") @VisibleForTesting void sendInitialized() { - if (isInitialized) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("duration", exoPlayer.getDuration()); - - VideoSize videoSize = exoPlayer.getVideoSize(); - int width = videoSize.width; - int height = videoSize.height; - if (width != 0 && height != 0) { - int rotationDegrees = videoSize.unappliedRotationDegrees; - // Switch the width/height if video was taken in portrait mode - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = videoSize.height; - height = videoSize.width; - } - event.put("width", width); - event.put("height", height); - - // Rotating the video with ExoPlayer does not seem to be possible with a Surface, - // so inform the Flutter code that the widget needs to be rotated to prevent - // upside-down playback for videos with rotationDegrees of 180 (other orientations work - // correctly without correction). - if (rotationDegrees == 180) { - event.put("rotationCorrection", rotationDegrees); - } + if (!isInitialized) { + return; + } + VideoSize videoSize = exoPlayer.getVideoSize(); + int rotationCorrection = 0; + int width = videoSize.width; + int height = videoSize.height; + if (width != 0 && height != 0) { + int rotationDegrees = videoSize.unappliedRotationDegrees; + // Switch the width/height if video was taken in portrait mode + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = videoSize.height; + height = videoSize.width; + } + // Rotating the video with ExoPlayer does not seem to be possible with a Surface, + // so inform the Flutter code that the widget needs to be rotated to prevent + // upside-down playback for videos with rotationDegrees of 180 (other orientations work + // correctly without correction). + if (rotationDegrees == 180) { + rotationCorrection = rotationDegrees; } - - eventSink.success(event); } + videoPlayerEvents.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); } void dispose() { @@ -284,7 +249,6 @@ void dispose() { exoPlayer.stop(); } textureEntry.release(); - eventChannel.setStreamHandler(null); if (surface != null) { surface.release(); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java new file mode 100644 index 00000000000..b3a1a3967d8 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -0,0 +1,33 @@ +// 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.plugins.videoplayer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Callbacks representing events invoked by {@link VideoPlayer}. + * + *

In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the + * expected events to send back through the plugin channel. In tests methods can be overridden in + * order to assert results. + * + *

See {@link androidx.media3.common.Player.Listener} for details. + */ +interface VideoPlayerCallbacks { + void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees); + + void onBufferingStart(); + + void onBufferingUpdate(long bufferedPosition); + + void onBufferingEnd(); + + void onCompleted(); + + void onError(@NonNull String code, @Nullable String message, @Nullable Object details); + + void onIsPlayingStateUpdate(boolean isPlaying); +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java new file mode 100644 index 00000000000..bc9041eedd1 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -0,0 +1,98 @@ +// 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.plugins.videoplayer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.EventChannel; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks { + private final EventChannel.EventSink eventSink; + + static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) { + QueuingEventSink eventSink = new QueuingEventSink(); + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + eventSink.setDelegate(events); + } + + @Override + public void onCancel(Object arguments) { + eventSink.setDelegate(null); + } + }); + return VideoPlayerEventCallbacks.withSink(eventSink); + } + + @VisibleForTesting + static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { + return new VideoPlayerEventCallbacks(eventSink); + } + + private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + } + + @Override + public void onInitialized( + int width, int height, long durationInMs, int rotationCorrectionInDegrees) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("width", width); + event.put("height", height); + event.put("duration", durationInMs); + if (rotationCorrectionInDegrees != 0) { + event.put("rotationCorrection", rotationCorrectionInDegrees); + } + eventSink.success(event); + } + + @Override + public void onBufferingStart() { + Map event = new HashMap<>(); + event.put("event", "bufferingStart"); + eventSink.success(event); + } + + @Override + public void onBufferingUpdate(long bufferedPosition) { + // iOS supports a list of buffered ranges, so we send as a list with a single range. + Map event = new HashMap<>(); + event.put("values", Collections.singletonList(bufferedPosition)); + eventSink.success(event); + } + + @Override + public void onBufferingEnd() { + Map event = new HashMap<>(); + event.put("event", "bufferingEnd"); + eventSink.success(event); + } + + @Override + public void onCompleted() { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } + + @Override + public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) { + eventSink.error(code, message, details); + } + + @Override + public void onIsPlayingStateUpdate(boolean isPlaying) { + Map event = new HashMap<>(); + event.put("isPlaying", isPlaying); + eventSink.success(event); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 5259e1ad3fe..62dca403ac4 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -125,7 +125,7 @@ public void initialize() { player = new VideoPlayer( flutterState.applicationContext, - eventChannel, + VideoPlayerEventCallbacks.bindTo(eventChannel), handle, "asset:///" + assetLookupKey, null, @@ -136,7 +136,7 @@ public void initialize() { player = new VideoPlayer( flutterState.applicationContext, - eventChannel, + VideoPlayerEventCallbacks.bindTo(eventChannel), handle, arg.getUri(), arg.getFormatHint(), diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index a7a03e9e1ed..72def743d51 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -19,12 +19,12 @@ import androidx.media3.common.VideoSize; import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.exoplayer.ExoPlayer; -import io.flutter.plugin.common.EventChannel; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,38 +37,41 @@ @RunWith(RobolectricTestRunner.class) public class VideoPlayerTest { private ExoPlayer fakeExoPlayer; - private EventChannel fakeEventChannel; private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry; - private SurfaceTexture fakeSurfaceTexture; private VideoPlayerOptions fakeVideoPlayerOptions; private QueuingEventSink fakeEventSink; private DefaultHttpDataSource.Factory httpDataSourceFactorySpy; @Captor private ArgumentCaptor> eventCaptor; + private AutoCloseable mocks; + @Before public void before() { - MockitoAnnotations.openMocks(this); + mocks = MockitoAnnotations.openMocks(this); fakeExoPlayer = mock(ExoPlayer.class); - fakeEventChannel = mock(EventChannel.class); fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class); - fakeSurfaceTexture = mock(SurfaceTexture.class); + SurfaceTexture fakeSurfaceTexture = mock(SurfaceTexture.class); when(fakeSurfaceTextureEntry.surfaceTexture()).thenReturn(fakeSurfaceTexture); fakeVideoPlayerOptions = mock(VideoPlayerOptions.class); fakeEventSink = mock(QueuingEventSink.class); httpDataSourceFactorySpy = spy(new DefaultHttpDataSource.Factory()); } + @After + public void after() throws Exception { + mocks.close(); + } + @Test public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); videoPlayer.configureHttpDataSourceFactory(new HashMap<>()); @@ -84,10 +87,9 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); Map httpHeaders = new HashMap() { @@ -110,10 +112,9 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); Map httpHeaders = new HashMap() { @@ -134,10 +135,9 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 90, 1f); @@ -148,13 +148,15 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); + + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 200); - assertEquals(event.get("height"), 100); - assertEquals(event.get("rotationCorrection"), null); + assertEquals(expected, actual); } @Test @@ -162,10 +164,9 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 270, 1f); @@ -176,13 +177,15 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 200); - assertEquals(event.get("height"), 100); - assertEquals(event.get("rotationCorrection"), null); + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); + + assertEquals(expected, actual); } @Test @@ -190,10 +193,9 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 0, 1f); @@ -204,13 +206,15 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); + + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 100); + expected.put("height", 200); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 100); - assertEquals(event.get("height"), 200); - assertEquals(event.get("rotationCorrection"), null); + assertEquals(expected, actual); } @Test @@ -218,10 +222,9 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 180, 1f); @@ -232,13 +235,16 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); + + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 100); + expected.put("height", 200); + expected.put("rotationCorrection", 180); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 100); - assertEquals(event.get("height"), 200); - assertEquals(event.get("rotationCorrection"), 180); + assertEquals(expected, actual); } @Test @@ -246,10 +252,9 @@ public void onIsPlayingChangedSendsExpectedEvent() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); doAnswer( @@ -257,7 +262,7 @@ public void onIsPlayingChangedSendsExpectedEvent() { invocation -> { Map event = new HashMap<>(); event.put("event", "isPlayingStateUpdate"); - event.put("isPlaying", (Boolean) invocation.getArguments()[0]); + event.put("isPlaying", invocation.getArguments()[0]); fakeEventSink.success(event); return null; }) @@ -288,13 +293,13 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() { .when(fakeExoPlayer) .addListener(any()); + @SuppressWarnings("unused") VideoPlayer unused = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); PlaybackException exception = @@ -304,4 +309,28 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() { verify(fakeExoPlayer).seekToDefaultPosition(); verify(fakeExoPlayer).prepare(); } + + @Test + public void otherErrorsReportVideoErrorWithErrorString() { + List listeners = new LinkedList<>(); + doAnswer(invocation -> listeners.add(invocation.getArgument(0))) + .when(fakeExoPlayer) + .addListener(any()); + + @SuppressWarnings("unused") + VideoPlayer unused = + new VideoPlayer( + fakeExoPlayer, + VideoPlayerEventCallbacks.withSink(fakeEventSink), + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + httpDataSourceFactorySpy); + + PlaybackException exception = + new PlaybackException( + "You did bad kid", null, PlaybackException.ERROR_CODE_DECODING_FAILED); + listeners.forEach(listener -> listener.onPlayerError(exception)); + + verify(fakeEventSink).error(eq("VideoError"), contains("You did bad kid"), any()); + } }